feat(ui) - dashboard - other widgets

This commit is contained in:
Shekar Siri 2022-04-14 19:23:08 +02:00
parent 2c3c0c30c0
commit dc18805699
22 changed files with 379 additions and 12 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -117,5 +117,4 @@ function DashboardSideMenu(props: Props) {
));
}
export default connect((state) => {
}, { setShowAlerts })(withRouter(DashboardSideMenu));
export default connect(null, { setShowAlerts })(withRouter(DashboardSideMenu));

View file

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