feat(ui) - dashboard - wip

This commit is contained in:
Shekar Siri 2022-04-08 16:43:55 +02:00
parent ff3e185c43
commit 351d1749e9
60 changed files with 1313 additions and 250 deletions

View file

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

View file

@ -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(() => {

View file

@ -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(() => {

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = {

View file

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

View file

@ -1,5 +1,5 @@
.bar {
height: 10px;
height: 5px;
background-color: red;
width: 100%;
border-radius: 3px;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(() => {

View file

@ -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(() => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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