feat(ui) - dashboard - wip
This commit is contained in:
parent
ff3e185c43
commit
351d1749e9
60 changed files with 1313 additions and 250 deletions
|
|
@ -69,12 +69,16 @@ export default class APIClient {
|
|||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
fetch(path, params, options = { clean: true }) {
|
||||
fetch(path, params, options = { clean: true }) {
|
||||
if (params !== undefined) {
|
||||
const cleanedParams = options.clean ? clean(params) : params;
|
||||
this.init.body = JSON.stringify(cleanedParams);
|
||||
}
|
||||
|
||||
if (this.init.method === 'GET') {
|
||||
delete this.init.body;
|
||||
}
|
||||
|
||||
|
||||
let fetch = window.fetch;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function AlertFormModal(props: Props) {
|
|||
const onDelete = async (instance) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`
|
||||
})) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const Alerts = props => {
|
|||
const onDelete = async (instance) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`
|
||||
})) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class SlackAddForm extends React.PureComponent {
|
|||
remove = async (id) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this channel?`
|
||||
})) {
|
||||
this.props.remove(id);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
onClick?: (event, index) => void;
|
||||
}
|
||||
function CustomMetriLineChart(props: Props) {
|
||||
const { data, params, seriesMap, colors, onClick = () => null } = props;
|
||||
const { data, params, seriesMap = [], colors, onClick = () => null } = props;
|
||||
return (
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
import React from 'react'
|
||||
import { Styles } from '../../common';
|
||||
import { AreaChart, ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||
import { LineChart, Line, Legend } from 'recharts';
|
||||
import cn from 'classnames';
|
||||
import CountBadge from '../../common/CountBadge';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
params: any;
|
||||
seriesMap: any;
|
||||
colors: any;
|
||||
onClick?: (event, index) => void;
|
||||
}
|
||||
function CustomMetricOverviewChart(props: Props) {
|
||||
const { data, params, seriesMap, colors, onClick = () => null } = props;
|
||||
console.log('data', data)
|
||||
const gradientDef = Styles.gradientDef();
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className="absolute flex items-start flex-col justify-center inset-0 p-3">
|
||||
<div className="mb-2 flex items-center" >
|
||||
{/* <div className={ cn("text-lg") }>{ 'test' }</div> */}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{/* {prefix} */}
|
||||
{/* <div className="h-2 w-2 bg-red mr-2" />
|
||||
<div className="h-2 w-2 bg-green mr-2 rounded-full" />
|
||||
<div className="mr-2" style={{ borderWidth: "0 5px 7px 5px", borderColor: "transparent transparent #007bff transparent" }} /> */}
|
||||
<CountBadge
|
||||
// title={subtext}
|
||||
count={ countView(Math.round(data.value), data.unit) }
|
||||
change={ data.progress || 0 }
|
||||
unit={ data.unit }
|
||||
// className={textClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ResponsiveContainer height={ 100 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
// syncId={syncId}
|
||||
margin={ {
|
||||
top: 85, right: 0, left: 0, bottom: 5,
|
||||
} }
|
||||
>
|
||||
{gradientDef}
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<XAxis hide {...Styles.xaxis} interval={4} dataKey="time" />
|
||||
<YAxis hide interval={ 0 } />
|
||||
<Area
|
||||
name={''}
|
||||
// unit={unit && ' ' + unit}
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomMetricOverviewChart
|
||||
|
||||
|
||||
const countView = (avg, unit) => {
|
||||
if (unit === 'mb') {
|
||||
if (!avg) return 0;
|
||||
const count = Math.trunc(avg / 1024 / 1024);
|
||||
return numberWithCommas(count);
|
||||
}
|
||||
if (unit === 'min') {
|
||||
if (!avg) return 0;
|
||||
const count = Math.trunc(avg);
|
||||
return numberWithCommas(count > 1000 ? count +'k' : count);
|
||||
}
|
||||
return avg ? numberWithCommas(avg): 0;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricOverviewChart';
|
||||
|
|
@ -56,29 +56,29 @@ function CustomMetricWidget(props: Props) {
|
|||
const isTable = metric.viewType === 'table';
|
||||
const isPieChart = metric.viewType === 'pieChart';
|
||||
|
||||
useEffect(() => {
|
||||
new APIClient()['post'](`/custom_metrics/${metricParams.metricId}/chart`, { ...metricParams, q: metric.name })
|
||||
.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;
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// new APIClient()['post'](`/custom_metrics/${metricParams.metricId}/chart`, { ...metricParams, q: metric.name })
|
||||
// .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));
|
||||
}, [period])
|
||||
// setSeriesMap(namesMap);
|
||||
// setData(getChartFormatter(period)(data));
|
||||
// }
|
||||
// }).finally(() => setLoading(false));
|
||||
// }, [period])
|
||||
|
||||
const clickHandlerTable = (filters) => {
|
||||
const activeWidget = {
|
||||
|
|
|
|||
|
|
@ -61,27 +61,27 @@ function CustomMetricWidget(props: Props) {
|
|||
setLoading(true);
|
||||
|
||||
// fetch new data for the widget preview
|
||||
new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
|
||||
.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;
|
||||
}, []);
|
||||
// new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
|
||||
// .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));
|
||||
// setSeriesMap(namesMap);
|
||||
// setData(getChartFormatter(period)(data));
|
||||
// }
|
||||
// }).finally(() => setLoading(false));
|
||||
}, [metric])
|
||||
|
||||
const onDateChange = (changedDates) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.bar {
|
||||
height: 10px;
|
||||
height: 5px;
|
||||
background-color: red;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const Bar = ({ className = '', width = 0, avg, domain, color }) => {
|
|||
<span className="font-medium">{`${avg}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm leading-3">{domain}</div>
|
||||
<div className="text-sm leading-3 color-gray-medium">{domain}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CPULoad(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CPULoad;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CPULoad'
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CallsErrors4xx(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{/* { data.namesMap.map((key, index) => (
|
||||
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
|
||||
))} */}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CallsErrors4xx;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CallsErrors4xx'
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CallsErrors5xx(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{/* { data.namesMap.map((key, index) => (
|
||||
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
|
||||
))} */}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CallsErrors5xx;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CallsErrors5xx'
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function Crashes(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Crashes"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Crashes;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Crashes'
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'pagesDomBuildtime';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function DomBuildingTime(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(DomBuildingTime)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DomBuildingTime'
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ErrorsByOrigin(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name={<span className="float">1<sup>st</sup> Party</span>} dataKey="firstParty" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar name={<span className="float">3<sup>rd</sup> Party</span>} dataKey="thirdParty" stackId="a" fill={Styles.colors[2]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsByOrigin;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorsByOrigin'
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ErrorsByType(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name="Integrations" dataKey="integrations" stackId="a" fill={Styles.colors[0]}/>
|
||||
<Bar name="4xx" dataKey="4xx" stackId="a" fill={Styles.colors[1]} />
|
||||
<Bar name="5xx" dataKey="5xx" stackId="a" fill={Styles.colors[2]} />
|
||||
<Bar name="Javascript" dataKey="js" stackId="a" fill={Styles.colors[3]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsByType;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorsByType'
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import Bar from 'App/components/Dashboard/Widgets/ErrorsPerDomain/Bar';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ErrorsPerDomain(props: Props) {
|
||||
const { data } = props;
|
||||
console.log('ErrorsPerDomain', data);
|
||||
// const firstAvg = 10;
|
||||
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{data.chart.map((item, i) =>
|
||||
<Bar
|
||||
key={i}
|
||||
className="mb-2"
|
||||
avg={numberWithCommas(Math.round(item.errorsCount))}
|
||||
width={Math.round((item.errorsCount * 100) / firstAvg) - 10}
|
||||
domain={item.domain}
|
||||
color={Styles.colors[i]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsPerDomain;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorsPerDomain'
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function FPS(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
<AvgLabel text="Avg" className="ml-3" count={data.avgFps} />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
dataKey="avgFps"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default FPS;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FPS'
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function MemoryConsumption(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
<AvgLabel text="Avg" unit="mb" className="ml-3" count={data.avgUsedJsHeapSize} />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "JS Heap Size (mb)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
unit=" mb"
|
||||
type="monotone"
|
||||
dataKey="avgFps"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemoryConsumption;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MemoryConsumption'
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'pagesResponseTime';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function ResponseTime(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Page Response Time (ms)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit=" ms"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(ResponseTime)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResponseTime'
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function SessionsAffectedByJSErrors(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name="Sessions" dataKey="sessionsCount" stackId="a" fill={Styles.colors[0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsAffectedByJSErrors;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionsAffectedByJSErrors'
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import Bar from 'App/components/Dashboard/Widgets/SlowestDomains/Bar';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function SlowestDomains(props: Props) {
|
||||
const { data } = props;
|
||||
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{data.chart.map((item, i) =>
|
||||
<Bar
|
||||
key={i}
|
||||
className="mb-2"
|
||||
avg={numberWithCommas(Math.round(item.errorsCount))}
|
||||
width={Math.round((item.errorsCount * 100) / firstAvg) - 10}
|
||||
domain={item.domain}
|
||||
color={Styles.colors[i]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SlowestDomains;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SlowestDomains'
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'timeToRender';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function TimeToRender(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
// allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Time to Render (ms)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit=" ms"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(TimeToRender)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './TimeToRender'
|
||||
|
|
@ -87,15 +87,19 @@ function DashboardMetricSelection(props) {
|
|||
</div>
|
||||
</div>
|
||||
<div className="col-span-9">
|
||||
<div className="grid grid-cols-2 gap-4 -mx-4 px-4 lg:grid-cols-2 sm:grid-cols-1">
|
||||
<div
|
||||
className="grid grid-cols-4 gap-4 -mx-4 px-4 pb-20 items-start"
|
||||
style={{ height: "calc(100vh - 165px)", overflowY: 'auto' }}
|
||||
>
|
||||
{activeCategory && activeCategory.widgets.map((widget: any) => (
|
||||
<div
|
||||
<WidgetWrapper
|
||||
key={widget.metricId}
|
||||
className={cn("rounded cursor-pointer")}
|
||||
widget={widget}
|
||||
active={selectedWidgetIds.includes(widget.metricId)}
|
||||
isTemplate={true}
|
||||
isWidget={widget.metricType === 'predefined'}
|
||||
onClick={() => dashboardStore.toggleWidgetSelection(widget)}
|
||||
>
|
||||
<WidgetWrapper widget={widget} active={selectedWidgetIds.includes(widget.metricId)} isTemplate={true}/>
|
||||
</div>
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useObserver, observer, useLocalObservable } from 'mobx-react-lite';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -7,6 +7,7 @@ import { withSiteId, dashboardSelected, metrics } from 'App/routes';
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import DashbaordListModal from '../DashbaordListModal';
|
||||
import DashboardModal from '../DashboardModal';
|
||||
import cn from 'classnames';
|
||||
|
||||
const SHOW_COUNT = 5;
|
||||
interface Props {
|
||||
|
|
@ -36,20 +37,25 @@ function DashboardSideMenu(props: Props) {
|
|||
showModal(<DashboardModal />, {})
|
||||
}
|
||||
|
||||
const togglePinned = (dashboard) => {
|
||||
dashboardStore.updatePinned(dashboard.dashboardId);
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div>
|
||||
<SideMenuHeader className="mb-4" text="Dashboards" />
|
||||
{dashboardsPicked.map((item: any) => (
|
||||
{dashboardsPicked.sort((a: any, b: any) => a.isPinned === b.isPinned ? 0 : a.isPinned ? -1 : 1 ).map((item: any) => (
|
||||
<SideMenuitem
|
||||
key={ item.dashboardId }
|
||||
active={item.dashboardId === dashboardId}
|
||||
title={ item.name }
|
||||
iconName={ item.icon }
|
||||
onClick={() => onItemClick(item)}
|
||||
className="group"
|
||||
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 className={cn("p-1 group-hover:visible", { 'invisible' : !item.isPinned })} onClick={() => togglePinned(item)}><Icon name="pin-fill" size="16" /></div>}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { withRouter } from 'react-router-dom';
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import DashboardModal from '../DashboardModal';
|
||||
import DashboardEditModal from '../DashboardEditModal';
|
||||
import DateRange from 'Shared/DateRange';
|
||||
|
||||
interface Props {
|
||||
siteId: number;
|
||||
|
|
@ -23,6 +24,7 @@ function DashboardView(props: Props) {
|
|||
const { hideModal, showModal } = useModal();
|
||||
const loading = useObserver(() => dashboardStore.fetchingDashboard);
|
||||
const dashboard: any = dashboardStore.selectedDashboard
|
||||
const period = useObserver(() => dashboardStore.period);
|
||||
const [showEditModal, setShowEditModal] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -42,7 +44,7 @@ function DashboardView(props: Props) {
|
|||
const onDelete = async () => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this Dashboard?`
|
||||
})) {
|
||||
dashboardStore.deleteDashboard(dashboard).then(() => {
|
||||
|
|
@ -53,7 +55,7 @@ function DashboardView(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
return (
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={!dashboard || !dashboard.dashboardId}
|
||||
|
|
@ -63,16 +65,26 @@ function DashboardView(props: Props) {
|
|||
<div>
|
||||
<DashboardEditModal
|
||||
show={showEditModal}
|
||||
// dashboard={dashboard}
|
||||
closeHandler={() => setShowEditModal(false)}
|
||||
/>
|
||||
<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={onAddWidgets}>Add Metric</Button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 color-gray-medium">Time Range</span>
|
||||
<DateRange
|
||||
rangeValue={period.rangeName}
|
||||
startDate={period.start}
|
||||
endDate={period.end}
|
||||
onDateChange={(period) => dashboardStore.setPeriod(period)}
|
||||
customRangeRight
|
||||
direction="left"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4" />
|
||||
<ItemMenu
|
||||
items={[
|
||||
{
|
||||
|
|
@ -95,7 +107,7 @@ function DashboardView(props: Props) {
|
|||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
export default withRouter(withModal(observer(DashboardView)));
|
||||
|
|
@ -29,7 +29,7 @@ function DashboardWidgetGrid(props) {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-4 grid-cols-4 items-start pb-10">
|
||||
{list && list.map((item, index) => (
|
||||
<WidgetWrapper
|
||||
index={index}
|
||||
|
|
@ -38,6 +38,7 @@ function DashboardWidgetGrid(props) {
|
|||
moveListItem={(dragIndex, hoverIndex) => dashbaord.swapWidgetPosition(dragIndex, hoverIndex)}
|
||||
dashboardId={dashboardId}
|
||||
siteId={siteId}
|
||||
isWidget={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function MetricsSearch(props) {
|
|||
|
||||
return useObserver(() => (
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="18" />
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
|
||||
<input
|
||||
value={query}
|
||||
name="metricsSearch"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ interface Props{
|
|||
function MetricsView(props: Props) {
|
||||
const { siteId } = props;
|
||||
const { metricStore } = useStore();
|
||||
const metricsCount = useObserver(() => metricStore.metrics.length);
|
||||
|
||||
React.useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
|
|
@ -19,7 +20,10 @@ function MetricsView(props: Props) {
|
|||
return useObserver(() => (
|
||||
<div>
|
||||
<div className="flex items-center mb-4 justify-between">
|
||||
<PageTitle title="Metrics" className="mr-3" />
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Metrics" className="" />
|
||||
<span className="text-2xl color-gray-medium ml-2">{metricsCount}</span>
|
||||
</div>
|
||||
<Link to={'/metrics/create'}><Button primary size="small">Add Metric</Button></Link>
|
||||
<div className="ml-auto w-1/3">
|
||||
<MetricsSearch />
|
||||
|
|
|
|||
|
|
@ -1,72 +1,58 @@
|
|||
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 CustomMetriLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetriLineChart';
|
||||
import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
|
||||
import CustomMetricTable from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable';
|
||||
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart';
|
||||
import { Styles } from 'App/components/Dashboard/Widgets/common';
|
||||
import { observer, useObserver, useLocalObservable } from 'mobx-react-lite';
|
||||
import { Loader } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import WidgetPredefinedChart from '../WidgetPredefinedChart';
|
||||
interface Props {
|
||||
metric: any;
|
||||
isWidget?: boolean
|
||||
}
|
||||
function WidgetChart(props: Props) {
|
||||
const metric = useObserver(() => props.metric);
|
||||
const { metricStore } = useStore();
|
||||
// const metric: any = useObserver(() => metricStore.instance);
|
||||
const series = useObserver(() => metric.series);
|
||||
const { isWidget = false, metric } = props;
|
||||
// const metric = useObserver(() => props.metric);
|
||||
const { dashboardStore } = useStore();
|
||||
const period = useObserver(() => dashboardStore.period);
|
||||
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>();
|
||||
const [data, setData] = useState<any>(metric.data);
|
||||
|
||||
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]);
|
||||
setLoading(true);
|
||||
const data = isWidget ? {} : { ...metricParams, ...metric.toJson() };
|
||||
dashboardStore.fetchMetricChartData(metric, data, isWidget).then((res: any) => {
|
||||
setData(res);
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [period]);
|
||||
|
||||
const renderChart = () => {
|
||||
const { metricType, viewType } = metric;
|
||||
const { metricType, viewType, predefinedKey } = metric;
|
||||
|
||||
if (metricType === 'predefined') {
|
||||
return <WidgetPredefinedChart data={data} predefinedKey={metric.predefinedKey} />
|
||||
}
|
||||
|
||||
if (metricType === 'timeseries') {
|
||||
if (viewType === 'lineChart') {
|
||||
return (
|
||||
<CustomMetriLineChart
|
||||
data={data}
|
||||
data={metric.data}
|
||||
seriesMap={seriesMap}
|
||||
colors={colors}
|
||||
params={params}
|
||||
|
|
@ -75,7 +61,7 @@ function WidgetChart(props: Props) {
|
|||
} else if (viewType === 'progress') {
|
||||
return (
|
||||
<CustomMetricPercentage
|
||||
data={data[0]}
|
||||
data={metric.data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
|
|
@ -85,12 +71,12 @@ function WidgetChart(props: Props) {
|
|||
|
||||
if (metricType === 'table') {
|
||||
if (viewType === 'table') {
|
||||
return <CustomMetricTable metric={metric} data={data[0]} />;
|
||||
return <CustomMetricTable metric={metric} data={metric.data[0]} />;
|
||||
} else if (viewType === 'pieChart') {
|
||||
return (
|
||||
<CustomMetricPieChart
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
data={metric.data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
|
|
@ -100,11 +86,11 @@ function WidgetChart(props: Props) {
|
|||
|
||||
return <div>Unknown</div>;
|
||||
}
|
||||
return (
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
{renderChart()}
|
||||
</Loader>
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
export default observer(WidgetChart);
|
||||
export default WidgetChart;
|
||||
|
|
@ -70,7 +70,7 @@ function WidgetForm(props: Props) {
|
|||
const onDelete = async () => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this metric?`
|
||||
})) {
|
||||
metricStore.delete(metric).then(props.onDelete);
|
||||
|
|
@ -122,10 +122,10 @@ function WidgetForm(props: Props) {
|
|||
<>
|
||||
<span className="mx-3">issue type</span>
|
||||
<DropdownPlain
|
||||
name="metricValue"
|
||||
options={_issueOptions}
|
||||
value={ metric.metricValue[0] }
|
||||
onChange={ writeOption }
|
||||
name="metricValue"
|
||||
options={_issueOptions}
|
||||
value={ metric.metricValue[0] }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -134,12 +134,12 @@ function WidgetForm(props: Props) {
|
|||
<>
|
||||
<span className="mx-3">showing</span>
|
||||
<DropdownPlain
|
||||
name="metricFormat"
|
||||
options={[
|
||||
{ value: 'sessionCount', text: 'Session Count' },
|
||||
]}
|
||||
value={ metric.metricFormat }
|
||||
onChange={ writeOption }
|
||||
name="metricFormat"
|
||||
options={[
|
||||
{ value: 'sessionCount', text: 'Session Count' },
|
||||
]}
|
||||
value={ metric.metricFormat }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import React from 'react';
|
||||
import { Styles } from 'App/components/Dashboard/Widgets/common';
|
||||
import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart';
|
||||
import ErrorsByType from 'App/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType';
|
||||
import ErrorsByOrigin from 'App/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin';
|
||||
import ErrorsPerDomain from 'App/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import SessionsAffectedByJSErrors from '../../Widgets/PredefinedWidgets/SessionsAffectedByJSErrors';
|
||||
import CallsErrors4xx from '../../Widgets/PredefinedWidgets/CallsErrors4xx';
|
||||
import CallsErrors5xx from '../../Widgets/PredefinedWidgets/CallsErrors5xx';
|
||||
import CPULoad from '../../Widgets/PredefinedWidgets/CPULoad';
|
||||
import Crashes from '../../Widgets/PredefinedWidgets/Crashes';
|
||||
import DomBuildingTime from '../../Widgets/PredefinedWidgets/DomBuildingTime';
|
||||
import FPS from '../../Widgets/PredefinedWidgets/FPS';
|
||||
import MemoryConsumption from '../../Widgets/PredefinedWidgets/MemoryConsumption';
|
||||
import ResponseTime from '../../Widgets/PredefinedWidgets/ResponseTime';
|
||||
import TimeToRender from '../../Widgets/PredefinedWidgets/TimeToRender';
|
||||
import SlowestDomains from '../../Widgets/PredefinedWidgets/SlowestDomains';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
predefinedKey: string
|
||||
}
|
||||
function WidgetPredefinedChart(props: Props) {
|
||||
const { data, predefinedKey } = props;
|
||||
// const { viewType } = data;
|
||||
const params = { density: 70 }
|
||||
|
||||
const renderWidget = () => {
|
||||
switch (predefinedKey) {
|
||||
// ERRORS
|
||||
case 'errors_per_type':
|
||||
return <ErrorsByType data={data} />
|
||||
case 'errors_per_domains':
|
||||
return <ErrorsPerDomain data={data} />
|
||||
case 'resources_by_party':
|
||||
return <ErrorsByOrigin data={data} />
|
||||
case 'impacted_sessions_by_js_errors':
|
||||
return <SessionsAffectedByJSErrors data={data} />
|
||||
case 'domains_errors_4xx':
|
||||
return <CallsErrors4xx data={data} />
|
||||
case 'domains_errors_5xx':
|
||||
return <CallsErrors5xx data={data} />
|
||||
|
||||
// PERFORMANCE
|
||||
case 'cpu':
|
||||
return <CPULoad data={data} />
|
||||
case 'crashes':
|
||||
return <Crashes data={data} />
|
||||
case 'pages_dom_buildtime':
|
||||
return <DomBuildingTime data={data} />
|
||||
case 'fps':
|
||||
return <FPS data={data} />
|
||||
case 'memory_consumption':
|
||||
return <MemoryConsumption data={data} />
|
||||
case 'pages_response_time':
|
||||
return <ResponseTime data={data} />
|
||||
// case 'pages_response_time_distribution':
|
||||
// case 'resources_vs_visually_complete':
|
||||
// case 'impacted_sessions_by_slow_pages':
|
||||
// case 'sessions_per_browser':
|
||||
case 'slowest_domains':
|
||||
return <SlowestDomains data={data} />
|
||||
// case 'speed_location':
|
||||
case 'time_to_render':
|
||||
return <TimeToRender data={data} />
|
||||
|
||||
|
||||
default:
|
||||
return <div>No widget found</div>
|
||||
}
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<>
|
||||
{renderWidget()}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
export default WidgetPredefinedChart;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './WidgetPredefinedChart'
|
||||
|
|
@ -78,7 +78,7 @@ function WidgetPreview(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded p-4">
|
||||
<WidgetWrapper widget={metric} isPreview={true} />
|
||||
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} />
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -21,10 +21,13 @@ interface Props {
|
|||
siteId?: string,
|
||||
active?: boolean;
|
||||
history?: any
|
||||
onClick?: () => void;
|
||||
isWidget?: boolean;
|
||||
}
|
||||
function WidgetWrapper(props: Props) {
|
||||
const { dashboardStore } = useStore();
|
||||
const { active = false, widget, index = 0, moveListItem = null, isPreview = false, isTemplate = false, dashboardId, siteId } = props;
|
||||
const { isWidget = false, active = false, index = 0, moveListItem = null, isPreview = false, isTemplate = false, dashboardId, siteId } = props;
|
||||
const widget = useObserver(() => props.widget);
|
||||
|
||||
const [{ opacity, isDragging }, dragRef] = useDrag({
|
||||
type: 'item',
|
||||
|
|
@ -51,8 +54,8 @@ function WidgetWrapper(props: Props) {
|
|||
const onDelete = async () => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmation: `Are you sure you want to permanently delete this Dashboard?`
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete the widget from this dashboard?`
|
||||
})) {
|
||||
dashboardStore.deleteDashboardWidget(dashboardId!, widget.widgetId);
|
||||
}
|
||||
|
|
@ -63,7 +66,7 @@ function WidgetWrapper(props: Props) {
|
|||
}
|
||||
|
||||
const onChartClick = () => {
|
||||
if (isPreview || isTemplate) return;
|
||||
if (!isWidget || widget.metricType === 'predefined') return;
|
||||
props.history.push(withSiteId(dashboardMetricDetails(dashboardId, widget.metricId),siteId));
|
||||
}
|
||||
|
||||
|
|
@ -80,13 +83,14 @@ function WidgetWrapper(props: Props) {
|
|||
borderColor: (canDrop && isOver) || active ? '#394EFF' : (isPreview ? 'transparent' : '#EEEEEE'),
|
||||
}}
|
||||
ref={dragDropRef}
|
||||
onClick={props.onClick ? props.onClick : () => {}}
|
||||
>
|
||||
<div
|
||||
className="p-3 cursor-move flex items-center justify-between"
|
||||
>
|
||||
|
||||
<h3 className="capitalize">{widget.name}</h3>
|
||||
{!isPreview && !isTemplate && (
|
||||
{isWidget && (
|
||||
<div>
|
||||
<ItemMenu
|
||||
items={[
|
||||
|
|
@ -104,8 +108,8 @@ function WidgetWrapper(props: Props) {
|
|||
</div>
|
||||
|
||||
<LazyLoad height={300} offset={320} >
|
||||
<div className="px-2" onClick={onChartClick}>
|
||||
<WidgetChart metric={props.widget}/>
|
||||
<div className="px-4" onClick={onChartClick}>
|
||||
<WidgetChart metric={widget} isWidget={isWidget} />
|
||||
</div>
|
||||
</LazyLoad>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function SaveSearchModal(props: Props) {
|
|||
const onDelete = async () => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this Saved search?`,
|
||||
})) {
|
||||
props.remove(savedSearch.searchId).then(() => {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ function SavedSearchDropdown(props: Props) {
|
|||
const onDelete = async (instance) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this search?`
|
||||
})) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Icon } from 'UI';
|
||||
import styles from './itemMenu.css';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default class ItemMenu extends React.PureComponent {
|
||||
state = {
|
||||
|
|
@ -29,7 +30,7 @@ export default class ItemMenu extends React.PureComponent {
|
|||
>
|
||||
<div
|
||||
ref={ (ref) => { this.menuBtnRef = ref; } }
|
||||
className="w-10 h-10 cursor-pointer bg-white rounded-full flex items-center justify-center hover:bg-gray-lightest"
|
||||
className={cn("w-10 h-10 cursor-pointer rounded-full flex items-center justify-center hover:bg-gray-light", { 'bg-gray-light' : displayed })}
|
||||
onClick={ this.toggleMenu }
|
||||
role="button"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,17 @@ import Dashboard, { IDashboard } from "./types/dashboard"
|
|||
import Widget, { IWidget } from "./types/widget";
|
||||
import { dashboardService, metricService } from "App/services";
|
||||
import { toast } from 'react-toastify';
|
||||
import Period, { LAST_24_HOURS, LAST_7_DAYS } from 'Types/app/period';
|
||||
import { getChartFormatter } from 'Types/dashboard/helper';
|
||||
|
||||
export interface IDashboardSotre {
|
||||
dashboards: IDashboard[]
|
||||
selectedDashboard: IDashboard | null
|
||||
dashboardInstance: IDashboard
|
||||
selectedWidgets: IWidget[]
|
||||
startTimestamp: number
|
||||
endTimestamp: number
|
||||
period: Period
|
||||
|
||||
siteId: any
|
||||
currentWidget: Widget
|
||||
|
|
@ -52,6 +57,10 @@ export interface IDashboardSotre {
|
|||
fetchTemplates(): Promise<any>
|
||||
deleteDashboardWidget(dashboardId: string, widgetId: string): Promise<any>
|
||||
addWidgetToDashboard(dashboard: IDashboard, metricIds: any): Promise<any>
|
||||
|
||||
updatePinned(dashboardId: string): Promise<any>
|
||||
fetchMetricChartData(metric: IWidget, data: any, isWidget: boolean): Promise<any>
|
||||
setPeriod(period: any): void
|
||||
}
|
||||
export default class DashboardStore implements IDashboardSotre {
|
||||
siteId: any = null
|
||||
|
|
@ -63,6 +72,9 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
currentWidget: Widget = new Widget()
|
||||
widgetCategories: any[] = []
|
||||
widgets: Widget[] = []
|
||||
period: Period = Period({ rangeName: LAST_7_DAYS })
|
||||
startTimestamp: number = 0
|
||||
endTimestamp: number = 0
|
||||
|
||||
// Metrics
|
||||
metricsPage: number = 1
|
||||
|
|
@ -78,8 +90,6 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
widgetCategories: observable.ref,
|
||||
// dashboardInstance: observable.ref,
|
||||
|
||||
resetCurrentWidget: action,
|
||||
addDashboard: action,
|
||||
removeDashboard: action,
|
||||
|
|
@ -99,32 +109,11 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
removeSelectedWidgetByCategory: action,
|
||||
toggleWidgetSelection: action,
|
||||
fetchTemplates: action,
|
||||
updatePinned: action,
|
||||
setPeriod: action,
|
||||
|
||||
fetchMetricChartData: action
|
||||
})
|
||||
|
||||
|
||||
// TODO remove this sample data
|
||||
|
||||
// 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= 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)
|
||||
// }
|
||||
}
|
||||
|
||||
toggleAllSelectedWidgets(isSelected: boolean) {
|
||||
|
|
@ -180,7 +169,7 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
return dashboardService.getDashboards()
|
||||
.then((list: any) => {
|
||||
runInAction(() => {
|
||||
this.dashboards = list.map(d => new Dashboard().fromJson(d))
|
||||
this.dashboards = list.map(d => new Dashboard().fromJson(d)).sort((a, b) => a.position - b.position)
|
||||
})
|
||||
}).finally(() => {
|
||||
runInAction(() => {
|
||||
|
|
@ -383,53 +372,65 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
updatePinned(dashboardId: string): Promise<any> {
|
||||
// this.isSaving = true
|
||||
return dashboardService.updatePinned(dashboardId).then(() => {
|
||||
toast.success('Dashboard pinned successfully')
|
||||
this.dashboards.forEach(d => {
|
||||
if (d.dashboardId === dashboardId) {
|
||||
d.isPinned = true
|
||||
} else {
|
||||
d.isPinned = false
|
||||
}
|
||||
})
|
||||
}).catch(() => {
|
||||
toast.error('Dashboard could not be pinned')
|
||||
}).finally(() => {
|
||||
// this.isSaving = false
|
||||
})
|
||||
}
|
||||
|
||||
setPeriod(period: any) {
|
||||
this.period = Period({ start: period.startDate, end: period.endDate, rangeName: period.rangeValue })
|
||||
}
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
const sampleDashboards = [
|
||||
getRandomDashboard(1, true),
|
||||
getRandomDashboard(2),
|
||||
getRandomDashboard(3),
|
||||
getRandomDashboard(4),
|
||||
]
|
||||
fetchMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise<any> {
|
||||
const period = this.period.toTimestamps()
|
||||
return new Promise((resolve, reject) => {
|
||||
// this.isLoading = true
|
||||
return metricService.getMetricChartData(metric, { ...period, ...data, key: metric.predefinedKey }, isWidget)
|
||||
.then(data => {
|
||||
if (metric.metricType === 'predefined' && metric.viewType === 'overview') {
|
||||
const _data = { ...data, chart: getChartFormatter(this.period)(data.chart) }
|
||||
metric.setData(_data)
|
||||
resolve(_data);
|
||||
} else {
|
||||
if (metric.predefinedKey === 'errors_per_domains') {
|
||||
console.log('errors_per_domains', data)
|
||||
data.chart = data
|
||||
} else {
|
||||
data.chart = getChartFormatter(this.period)(Array.isArray(data) ? data : data.chart)
|
||||
}
|
||||
data.namesMap = Array.isArray(data) ? 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;
|
||||
}, []) : data.chart;
|
||||
console.log('map', data.namesMap)
|
||||
const _data = { namesMap: data.namesMap, chart: data.chart }
|
||||
metric.setData(_data)
|
||||
resolve(_data);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log('err', err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,6 @@ export interface IMetricStore {
|
|||
fetchList(): void
|
||||
fetch(metricId: string)
|
||||
delete(metric: IWidget)
|
||||
// fetchMetricChartData(metric: IWidget)
|
||||
}
|
||||
|
||||
export default class MetricStore implements IMetricStore {
|
||||
|
|
@ -131,7 +130,7 @@ export default class MetricStore implements IMetricStore {
|
|||
|
||||
// API Communication
|
||||
save(metric: IWidget, dashboardId?: string): Promise<any> {
|
||||
const wasCreating = !metric[Widget.ID_KEY]
|
||||
const wasCreating = !metric.exists()
|
||||
this.isSaving = true
|
||||
return metricService.saveMetric(metric, dashboardId)
|
||||
.then((metric) => {
|
||||
|
|
@ -177,20 +176,9 @@ export default class MetricStore implements IMetricStore {
|
|||
return metricService.deleteMetric(metric[Widget.ID_KEY])
|
||||
.then(() => {
|
||||
this.removeById(metric[Widget.ID_KEY])
|
||||
toast.success('Metric deleted successfully')
|
||||
}).finally(() => {
|
||||
this.isSaving = false
|
||||
})
|
||||
}
|
||||
|
||||
fetchMetricChartData(metric: IWidget) {
|
||||
this.isLoading = true
|
||||
return metricService.getMetricChartData(metric)
|
||||
.then(data => {
|
||||
// runInAction(() => {
|
||||
// metric.data = data
|
||||
// })
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { makeAutoObservable, observable, action, runInAction } from "mobx"
|
||||
import Widget, { IWidget } from "./widget"
|
||||
import { dashboardService } from "App/services"
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export interface IDashboard {
|
||||
dashboardId: any
|
||||
|
|
@ -24,7 +26,7 @@ export interface IDashboard {
|
|||
getWidgetByIndex(index: number): void
|
||||
getWidgetCount(): void
|
||||
getWidgetIndexByWidgetId(widgetId: string): void
|
||||
swapWidgetPosition(positionA: number, positionB: number): void
|
||||
swapWidgetPosition(positionA: number, positionB: number): Promise<any>
|
||||
sortWidgets(): void
|
||||
exists(): boolean
|
||||
toggleMetrics(metricId: string): void
|
||||
|
|
@ -93,7 +95,7 @@ export default class Dashboard implements IDashboard {
|
|||
this.name = json.name
|
||||
this.isPublic = json.isPublic
|
||||
this.isPinned = json.isPinned
|
||||
this.config = json.config
|
||||
// this.config = json.config
|
||||
this.widgets = json.widgets ? json.widgets.map(w => new Widget().fromJson(w)) : []
|
||||
})
|
||||
return this
|
||||
|
|
@ -138,7 +140,7 @@ export default class Dashboard implements IDashboard {
|
|||
return this.widgets.findIndex(w => w.widgetId === widgetId)
|
||||
}
|
||||
|
||||
swapWidgetPosition(positionA, positionB) {
|
||||
swapWidgetPosition(positionA, positionB): Promise<any> {
|
||||
const widgetA = this.widgets[positionA]
|
||||
const widgetB = this.widgets[positionB]
|
||||
this.widgets[positionA] = widgetB
|
||||
|
|
@ -146,6 +148,19 @@ export default class Dashboard implements IDashboard {
|
|||
|
||||
widgetA.position = positionB
|
||||
widgetB.position = positionA
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
Promise.all([
|
||||
dashboardService.saveWidget(this.dashboardId, widgetA),
|
||||
dashboardService.saveWidget(this.dashboardId, widgetB)
|
||||
]).then(() => {
|
||||
toast.success("Widget position updated")
|
||||
resolve()
|
||||
}).catch(() => {
|
||||
toast.error("Error updating widget position")
|
||||
reject()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
sortWidgets() {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface IWidget {
|
|||
metricType: string
|
||||
metricOf: string
|
||||
metricValue: string
|
||||
metricFormat: string
|
||||
viewType: string
|
||||
series: FilterSeries[]
|
||||
sessions: []
|
||||
|
|
@ -25,6 +26,7 @@ export interface IWidget {
|
|||
isValid: boolean
|
||||
dashboardId: any
|
||||
colSpan: number
|
||||
predefinedKey: string
|
||||
|
||||
udpateKey(key: string, value: any): void
|
||||
removeSeries(index: number): void
|
||||
|
|
@ -34,6 +36,8 @@ export interface IWidget {
|
|||
validate(): void
|
||||
update(data: any): void
|
||||
exists(): boolean
|
||||
toWidget(): any
|
||||
setData(data: any): void
|
||||
}
|
||||
export default class Widget implements IWidget {
|
||||
public static get ID_KEY():string { return "metricId" }
|
||||
|
|
@ -44,6 +48,7 @@ export default class Widget implements IWidget {
|
|||
metricOf: string = "sessionCount"
|
||||
metricValue: string = ""
|
||||
viewType: string = "lineChart"
|
||||
metricFormat: string = "sessionCount"
|
||||
series: FilterSeries[] = []
|
||||
sessions: [] = []
|
||||
isPublic: boolean = true
|
||||
|
|
@ -54,15 +59,19 @@ export default class Widget implements IWidget {
|
|||
config: any = {}
|
||||
|
||||
position: number = 0
|
||||
data: any = {}
|
||||
data: any = {
|
||||
chart: [],
|
||||
namesMap: {}
|
||||
}
|
||||
isLoading: boolean = false
|
||||
isValid: boolean = false
|
||||
dashboardId: any = undefined
|
||||
colSpan: number = 2
|
||||
predefinedKey: string = ''
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
// data: observable,
|
||||
data: observable.ref,
|
||||
widgetId: observable,
|
||||
name: observable,
|
||||
metricType: observable,
|
||||
|
|
@ -108,6 +117,8 @@ export default class Widget implements IWidget {
|
|||
this.metricValue = json.metricValue
|
||||
this.metricOf = json.metricOf
|
||||
this.metricType = json.metricType
|
||||
this.metricFormat = json.metricFormat
|
||||
this.viewType = json.viewType
|
||||
this.name = json.name
|
||||
this.series = json.series ? json.series.map((series: any) => new FilterSeries().fromJson(series)) : [],
|
||||
this.dashboards = json.dashboards
|
||||
|
|
@ -116,10 +127,21 @@ export default class Widget implements IWidget {
|
|||
this.lastModified = DateTime.fromMillis(1649319074)
|
||||
this.config = json.config
|
||||
this.position = json.config.position
|
||||
this.predefinedKey = json.predefinedKey
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
toWidget(): any {
|
||||
return {
|
||||
config: {
|
||||
position: this.position,
|
||||
col: this.config.col,
|
||||
row: this.config.row,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
metricId: this.metricId,
|
||||
|
|
@ -127,6 +149,7 @@ export default class Widget implements IWidget {
|
|||
metricOf: this.metricOf,
|
||||
metricValue: this.metricValue,
|
||||
metricType: this.metricType,
|
||||
metricFormat: this.metricFormat,
|
||||
viewType: this.viewType,
|
||||
name: this.name,
|
||||
series: this.series.map((series: any) => series.toJson()),
|
||||
|
|
@ -146,4 +169,10 @@ export default class Widget implements IWidget {
|
|||
exists() {
|
||||
return this.metricId !== undefined
|
||||
}
|
||||
|
||||
setData(data: any) {
|
||||
runInAction(() => {
|
||||
Object.assign(this.data, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ export interface IDashboardService {
|
|||
addWidget(dashboard: IDashboard, metricIds: []): Promise<any>
|
||||
saveWidget(dashboardId: string, widget: IWidget): Promise<any>
|
||||
deleteWidget(dashboardId: string, widgetId: string): Promise<any>
|
||||
|
||||
updatePinned(dashboardId: string): Promise<any>
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -115,8 +117,6 @@ export default class DashboardService implements IDashboardService {
|
|||
*/
|
||||
saveMetric(metric: IWidget, dashboardId?: string): Promise<any> {
|
||||
const data = metric.toJson();
|
||||
|
||||
// const path = dashboardId ? `/metrics` : '/metrics'; // TODO change to /dashboards/:dashboardId/widgets
|
||||
const path = dashboardId ? `/dashboards/${dashboardId}/metrics` : '/metrics';
|
||||
if (metric.widgetId) {
|
||||
return this.client.put(path + '/' + metric.widgetId, data)
|
||||
|
|
@ -142,6 +142,23 @@ export default class DashboardService implements IDashboardService {
|
|||
* @returns {Promise<any>}
|
||||
*/
|
||||
saveWidget(dashboardId: string, widget: IWidget): Promise<any> {
|
||||
return this.client.post(`/dashboards/${dashboardId}/widgets`, widget.toJson())
|
||||
if (widget.widgetId) {
|
||||
return this.client.put(`/dashboards/${dashboardId}/widgets/${widget.widgetId}`, widget.toWidget())
|
||||
.then(response => response.json())
|
||||
.then(response => response.data || {});
|
||||
}
|
||||
return this.client.post(`/dashboards/${dashboardId}/widgets`, widget.toWidget())
|
||||
.then(response => response.json())
|
||||
.then(response => response.data || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the pinned status of a dashboard.
|
||||
* @param dashboardId
|
||||
* @returns
|
||||
*/
|
||||
updatePinned(dashboardId: string): Promise<any> {
|
||||
return this.client.get(`/dashboards/${dashboardId}/pin`, {})
|
||||
.then(response => response.json())
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ export interface IMetricService {
|
|||
deleteMetric(metricId: string): Promise<any>;
|
||||
|
||||
getTemplates(): Promise<any>;
|
||||
getMetricChartData(metric: IWidget): Promise<any>;
|
||||
getMetricChartData(metric: IWidget, data: any, isWidget: boolean): Promise<any>;
|
||||
}
|
||||
|
||||
export default class MetricService implements IMetricService {
|
||||
|
|
@ -54,18 +54,10 @@ export default class MetricService implements IMetricService {
|
|||
const data = metric.toJson()
|
||||
const isCreating = !data[Widget.ID_KEY];
|
||||
const method = isCreating ? 'post' : 'put';
|
||||
|
||||
if(dashboardId) {
|
||||
const url = `/dashboards/${dashboardId}/metrics`;
|
||||
return this.client[method](url, data)
|
||||
.then(response => response.json())
|
||||
.then(response => response.data || {});
|
||||
} else {
|
||||
const url = isCreating ? '/metrics' : '/metrics/' + data[Widget.ID_KEY];
|
||||
return this.client[method](url, data)
|
||||
.then(response => response.json())
|
||||
.then(response => response.data || {});
|
||||
}
|
||||
const url = isCreating ? '/metrics' : '/metrics/' + data[Widget.ID_KEY];
|
||||
return this.client[method](url, data)
|
||||
.then(response => response.json())
|
||||
.then(response => response.data || {});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -90,9 +82,9 @@ export default class MetricService implements IMetricService {
|
|||
.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)
|
||||
getMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise<any> {
|
||||
const path = isWidget ? `/metrics/${metric.metricId}/chart` : `/metrics/try`;
|
||||
return this.client.post(path, data)
|
||||
.then(response => response.json())
|
||||
.then(response => response.data || {});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue