feat(ui) - dashboard - other widgets
This commit is contained in:
parent
2c3c0c30c0
commit
dc18805699
22 changed files with 379 additions and 12 deletions
|
|
@ -17,6 +17,7 @@ function CallsErrors4xx(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function CallsErrors5xx(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
|
|
|
|||
|
|
@ -37,13 +37,13 @@ function DomBuildingTime(props: Props) {
|
|||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
{/* <WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
/> */}
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(metric.data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function ErrorsByOrigin(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function ErrorsByType(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ function ResourceLoadingTime(props: Props) {
|
|||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
{/* <WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
|
|
@ -75,7 +75,7 @@ function ResourceLoadingTime(props: Props) {
|
|||
top: '12px',
|
||||
left: '170px',
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
|
|
|
|||
|
|
@ -38,13 +38,13 @@ function ResponseTime(props: Props) {
|
|||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
{/* <WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
/> */}
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(metric.data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function SessionsAffectedByJSErrors(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
|
|
|
|||
15
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestResources/Chart.js
vendored
Normal file
15
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestResources/Chart.js
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { AreaChart, Area } from 'recharts';
|
||||
import { Styles } from '../../common';
|
||||
|
||||
const Chart = ({ data, compare }) => {
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
return (
|
||||
<AreaChart width={ 90 } height={ 30 } data={ data.chart } >
|
||||
<Area type="monotone" dataKey="avg" stroke={colors[0]} fill={ colors[3] } fillOpacity={ 0.5 } />
|
||||
</AreaChart>
|
||||
);
|
||||
}
|
||||
|
||||
Chart.displayName = 'Chart';
|
||||
|
||||
export default Chart;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useState } from 'react'
|
||||
|
||||
const CopyPath = ({ data }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const copyHandler = () => {
|
||||
copy(data.url);
|
||||
setCopied(true);
|
||||
setTimeout(function() {
|
||||
setCopied(false)
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cursor-pointer color-teal" onClick={copyHandler}>
|
||||
{ copied ? 'Copied' : 'Copy Path'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyPath
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { Popup } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import styles from './imageInfo.css';
|
||||
|
||||
const supportedTypes = ['png', 'jpg', 'jpeg', 'svg'];
|
||||
|
||||
const ImageInfo = ({ data }) => {
|
||||
const canPreview = supportedTypes.includes(data.type);
|
||||
return (
|
||||
<div className={ styles.name }>
|
||||
<Popup
|
||||
className={ styles.popup }
|
||||
trigger={
|
||||
<div className={cn({ [styles.hasPreview]: canPreview})}>
|
||||
<div className={ styles.label }>{data.name}</div>
|
||||
</div>
|
||||
}
|
||||
disabled={!canPreview}
|
||||
content={ <img src={ `${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
ImageInfo.displayName = 'ImageInfo';
|
||||
|
||||
export default ImageInfo;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
const ResourceType = ({ data : { type = 'js' }, compare }) => {
|
||||
return (
|
||||
<div className={ cn("rounded-full p-2 color-white h-12 w-12 flex items-center justify-center", { 'bg-teal': compare, 'bg-tealx': !compare})}>
|
||||
<span className="overflow-hidden whitespace-no-wrap text-xs">{ type.toUpperCase() }</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResourceType
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, Table } from '../../common';
|
||||
import { List } from 'immutable';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
import Chart from './Chart';
|
||||
import ImageInfo from './ImageInfo';
|
||||
import ResourceType from './ResourceType';
|
||||
import CopyPath from './CopyPath';
|
||||
|
||||
export const RESOURCE_OPTIONS = [
|
||||
{ text: 'All', value: 'ALL', },
|
||||
{ text: 'CSS', value: 'STYLESHEET', },
|
||||
{ text: 'JS', value: 'SCRIPT', },
|
||||
];
|
||||
|
||||
const cols = [
|
||||
{
|
||||
key: 'type',
|
||||
title: 'Type',
|
||||
Component: ResourceType,
|
||||
className: 'text-center justify-center',
|
||||
cellClass: 'ml-2',
|
||||
width: '8%',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: 'File Name',
|
||||
Component: ImageInfo,
|
||||
cellClass: '-ml-2',
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
key: 'avg',
|
||||
title: 'Load Time',
|
||||
toText: avg => `${ avg ? numberWithCommas(Math.trunc(avg)) : 0} ms`,
|
||||
className: 'justify-center',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
key: 'trend',
|
||||
title: 'Trend',
|
||||
Component: Chart,
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
key: 'copy-path',
|
||||
title: '',
|
||||
Component: CopyPath,
|
||||
cellClass: 'invisible group-hover:visible text-right',
|
||||
width: '15%',
|
||||
}
|
||||
];
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
metric?: any
|
||||
}
|
||||
function MissingResources(props: Props) {
|
||||
const { data, metric } = props;
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
title="No resources missing."
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<div style={{ height: '240px', marginBottom:'10px'}}>
|
||||
<Table
|
||||
small
|
||||
cols={ cols }
|
||||
rows={ List(metric.data.chart) }
|
||||
rowClass="group"
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MissingResources;
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
& .label {
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.hasPreview {
|
||||
/* text-decoration: underline; */
|
||||
border-bottom: 1px dotted;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.imagePreview {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
margin-right: 10px;
|
||||
& > span {
|
||||
height: 16px;
|
||||
}
|
||||
& .label {
|
||||
font-size: 9px;
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #f5f5f5 !important;
|
||||
&:before {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SlowestResources'
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import { Styles } from '../../common';
|
||||
import cn from 'classnames';
|
||||
import stl from './scale.css';
|
||||
|
||||
function Scale({ colors }) {
|
||||
const lastIndex = (Styles.colors.length - 1)
|
||||
return (
|
||||
<div className={ cn(stl.bars, 'absolute bottom-0 mb-4')}>
|
||||
{colors.map((c, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{ backgroundColor: c, width: '6px', height: '15px', marginBottom: '1px' }}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
{ i === 0 && <div className="text-xs pl-12">Slow</div>}
|
||||
{ i === lastIndex && <div className="text-xs pl-12">Fast</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Scale
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import Scale from './Scale';
|
||||
import { threeLetter } from 'App/constants/countries';
|
||||
import { colorScale } from 'App/utils';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import * as DataMap from "datamaps";
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
// interface Props {
|
||||
// metric?: any
|
||||
// }
|
||||
function SpeedIndexByLocation(props) {
|
||||
const { metric } = props;
|
||||
const wrapper: any = React.useRef(null);
|
||||
let map: any = null;
|
||||
|
||||
const getSeries = data => {
|
||||
const series: any[] = [];
|
||||
data.forEach(item => {
|
||||
const d = [threeLetter[item.userCountry], Math.round(item.avg)]
|
||||
series.push(d)
|
||||
})
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (wrapper.current && !map && metric.data.chart.length > 0) {
|
||||
const dataset = getDataset();
|
||||
map = new DataMap({
|
||||
element: wrapper.current,
|
||||
fills: { defaultFill: '#E8E8E8' },
|
||||
data: dataset,
|
||||
// responsive: true,
|
||||
// height: null, //if not null, datamaps will grab the height of 'element'
|
||||
// width: null, //if not null, datamaps will grab the width of 'element'
|
||||
geographyConfig: {
|
||||
borderColor: '#FFFFFF',
|
||||
borderWidth: 0.5,
|
||||
highlightBorderWidth: 1,
|
||||
popupOnHover: true,
|
||||
// don't change color on mouse hover
|
||||
highlightFillColor: function(geo) {
|
||||
return '#999999';
|
||||
// return geo['fillColor'] || '#F5F5F5';
|
||||
},
|
||||
// only change border
|
||||
highlightBorderColor: '#B7B7B7',
|
||||
// show desired information in tooltip
|
||||
popupTemplate: function(geo, data) {
|
||||
// don't show tooltip if country don't present in dataset
|
||||
if (!data) { return ; }
|
||||
// tooltip content
|
||||
return ['<div class="hoverinfo speedIndexPopup">',
|
||||
'<strong>', geo.properties.name, '</strong>',
|
||||
'<span>Avg: <strong>', numberWithCommas(data.numberOfThings), '</strong><span>',
|
||||
'</div>'].join('');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (map) {
|
||||
// map.updateChoropleth(getSeries(metric.data.chart), { reset: true});
|
||||
// }
|
||||
// }, [])
|
||||
|
||||
const getDataset = () => {
|
||||
const { metric } = props;
|
||||
const colors = Styles.colors;
|
||||
|
||||
var dataset = {};
|
||||
const series = getSeries(metric.data.chart);
|
||||
var onlyValues = series.map(function(obj){ return obj[1]; });
|
||||
const paletteScale = colorScale(onlyValues, [...colors].reverse());
|
||||
|
||||
// fill dataset in appropriate format
|
||||
series.forEach(function(item){
|
||||
var iso = item[0], value = item[1];
|
||||
dataset[iso] = { numberOfThings: value, fillColor: paletteScale(value) };
|
||||
});
|
||||
return dataset;
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
<div className="w-full flex justify-end">
|
||||
<AvgLabel text="Avg" count={Math.round(metric.data.avg)} unit="ms" />
|
||||
</div>
|
||||
<Scale colors={Styles.colors} />
|
||||
<div
|
||||
style={{
|
||||
height: '220px',
|
||||
width: '90%',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
}}
|
||||
ref={ wrapper }
|
||||
/>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(SpeedIndexByLocation);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SpeedIndexByLocation'
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
.bars {
|
||||
& div:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
& div:last-child {
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
|
@ -38,13 +38,13 @@ function TimeToRender(props: Props) {
|
|||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
{/* <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={ 200 } width="100%">
|
||||
|
|
|
|||
|
|
@ -117,5 +117,4 @@ function DashboardSideMenu(props: Props) {
|
|||
));
|
||||
}
|
||||
|
||||
export default connect((state) => {
|
||||
}, { setShowAlerts })(withRouter(DashboardSideMenu));
|
||||
export default connect(null, { setShowAlerts })(withRouter(DashboardSideMenu));
|
||||
|
|
@ -24,6 +24,8 @@ import MissingResources from 'App/components/Dashboard/Widgets/PredefinedWidgets
|
|||
import ResourceLoadedVsResponseEnd from 'App/components/Dashboard/Widgets/PredefinedWidgets/ResourceLoadedVsResponseEnd';
|
||||
import SessionsPerBrowser from 'App/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser';
|
||||
import CallWithErrors from '../../Widgets/PredefinedWidgets/CallWithErrors';
|
||||
import SpeedIndexByLocation from '../../Widgets/PredefinedWidgets/SpeedIndexByLocation';
|
||||
import SlowestResources from '../../Widgets/PredefinedWidgets/SlowestResources';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
|
|
@ -54,7 +56,8 @@ function WidgetPredefinedChart(props: Props) {
|
|||
// PERFORMANCE
|
||||
// case 'impacted_sessions_by_slow_pages':
|
||||
// case 'pages_response_time_distribution':
|
||||
// case 'speed_location':
|
||||
case 'speed_location':
|
||||
return <SpeedIndexByLocation metric={metric} />
|
||||
case 'cpu':
|
||||
return <CPULoad data={data} metric={metric} />
|
||||
case 'crashes':
|
||||
|
|
@ -85,7 +88,8 @@ function WidgetPredefinedChart(props: Props) {
|
|||
return <ResourceLoadedVsResponseEnd data={data} metric={metric} />
|
||||
case 'resources_loading_time':
|
||||
return <ResourceLoadingTime data={data} metric={metric} />
|
||||
// case 'slowest_resources':
|
||||
case 'slowest_resources':
|
||||
return <SlowestResources data={data} metric={metric} />
|
||||
|
||||
default:
|
||||
return <div className="h-40 color-red">Widget not supported</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue