feat(ui) - custom metrics - wip

This commit is contained in:
Shekar Siri 2022-03-04 16:25:35 +01:00
parent a030b2b955
commit 1d957f90b5
11 changed files with 194 additions and 160 deletions

View file

@ -1,36 +1,8 @@
import React from 'react'
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
import { LineChart, Line, Legend, PieChart, Pie, Cell } from 'recharts';
import { ResponsiveContainer, Tooltip } from 'recharts';
import { PieChart, Pie, Cell } from 'recharts';
import { Styles } from '../../common';
function renderCustomizedLabel({
cx, cy, midAngle, innerRadius, outerRadius, value, color, startAngle, endAngle}) {
const RADIAN = Math.PI / 180;
const diffAngle = endAngle - startAngle;
const delta = ((360-diffAngle)/15)-1;
const radius = innerRadius + (outerRadius - innerRadius);
const x = cx + (radius+delta) * Math.cos(-midAngle * RADIAN);
const y = cy + (radius+(delta*delta)) * Math.sin(-midAngle * RADIAN);
return (
<text x={x} y={y} fill={color} textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central" fontSize={12} fontWeight="normal">
{value}
</text>
);
};
function renderCustomizedLabelLine(props){
let { cx, cy, midAngle, innerRadius, outerRadius, color, startAngle, endAngle } = props;
const RADIAN = Math.PI / 180;
const diffAngle = endAngle - startAngle;
const radius = 10 + innerRadius + (outerRadius - innerRadius);
let path='';
for(let i=0;i<((360-diffAngle)/15);i++){
path += `${(cx + (radius+i) * Math.cos(-midAngle * RADIAN))},${(cy + (radius+i*i) * Math.sin(-midAngle * RADIAN))} `
}
return (
<polyline points={path} stroke={color} fill="none" />
);
}
import { NoContent } from 'UI';
interface Props {
data: any;
params: any;
@ -42,47 +14,22 @@ interface Props {
function CustomMetricPieChart(props: Props) {
const { data = { values: [] }, params, colors, onClick = () => null } = props;
return (
<ResponsiveContainer height={ 240 } width="100%">
<PieChart>
<Pie
isAnimationActive={ false }
data={data.values}
dataKey="sessionCount"
nameKey="name"
cx="50%"
cy="50%"
// innerRadius={40}
outerRadius={70}
// fill={colors[0]}
activeIndex={1}
labelLine={({
cx,
cy,
midAngle,
innerRadius,
outerRadius,
value,
index
}) => {
const RADIAN = Math.PI / 180;
let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
let radius2 = innerRadius + (outerRadius - innerRadius);
let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
const percentage = value * 100 / data.values.reduce((a, b) => a + b.sessionCount, 0);
if (percentage<3){
return null;
}
return(
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#3EAAAF" strokeWidth={1} />
)
}}
label={({
<div>
<NoContent size="small" show={data.values.length === 0} >
<ResponsiveContainer height={ 220 } width="100%">
<PieChart>
<Pie
isAnimationActive={ false }
data={data.values}
dataKey="sessionCount"
nameKey="name"
cx="50%"
cy="50%"
// innerRadius={40}
outerRadius={70}
// fill={colors[0]}
activeIndex={1}
labelLine={({
cx,
cy,
midAngle,
@ -92,63 +39,94 @@ function CustomMetricPieChart(props: Props) {
index
}) => {
const RADIAN = Math.PI / 180;
let radius = 20 + innerRadius + (outerRadius - innerRadius);
let x = cx + radius * Math.cos(-midAngle * RADIAN);
let y = cy + radius * Math.sin(-midAngle * RADIAN);
const percentage = (value / data.values.reduce((a, b) => a + b.sessionCount, 0)) * 100;
if (percentage<3){
return null;
}
return (
<text
x={x}
y={y}
fontWeight="300"
fontSize="12px"
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
textAnchor={x > cx ? "start" : "end"}
dominantBaseline="central"
fill='#3EAAAF'
>
{data.values[index].name} - ({value})
</text>
);
}}
// label={({
// cx,
// cy,
// midAngle,
// innerRadius,
// outerRadius,
// value,
// index
// }) => {
// const RADIAN = Math.PI / 180;
// const radius = 30 + innerRadius + (outerRadius - innerRadius);
// const x = cx + radius * Math.cos(-midAngle * RADIAN);
// const y = cy + radius * Math.sin(-midAngle * RADIAN);
let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
let radius2 = innerRadius + (outerRadius - innerRadius);
let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
const percentage = value * 100 / data.values.reduce((a, b) => a + b.sessionCount, 0);
if (percentage<3){
return null;
}
return(
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#3EAAAF" strokeWidth={1} />
)
}}
label={({
cx,
cy,
midAngle,
innerRadius,
outerRadius,
value,
index
}) => {
const RADIAN = Math.PI / 180;
let radius = 20 + innerRadius + (outerRadius - innerRadius);
let x = cx + radius * Math.cos(-midAngle * RADIAN);
let y = cy + radius * Math.sin(-midAngle * RADIAN);
const percentage = (value / data.values.reduce((a, b) => a + b.sessionCount, 0)) * 100;
if (percentage<3){
return null;
}
return (
<text
x={x}
y={y}
fontWeight="400"
fontSize="12px"
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
textAnchor={x > cx ? "start" : "end"}
dominantBaseline="central"
fill='#666'
>
{data.values[index].name} - ({value})
</text>
);
}}
// label={({
// cx,
// cy,
// midAngle,
// innerRadius,
// outerRadius,
// value,
// index
// }) => {
// const RADIAN = Math.PI / 180;
// const radius = 30 + innerRadius + (outerRadius - innerRadius);
// const x = cx + radius * Math.cos(-midAngle * RADIAN);
// const y = cy + radius * Math.sin(-midAngle * RADIAN);
// return (
// <text
// x={x}
// y={y}
// fill="#3EAAAF"
// textAnchor={x > cx ? "start" : "end"}
// dominantBaseline="top"
// fontSize={10}
// >
// {data.values[index].name} ({value})
// </text>
// );
// }}
>
{data.values.map((entry, index) => (
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
))}
</Pie>
<Tooltip {...Styles.tooltip} />
</PieChart>
// return (
// <text
// x={x}
// y={y}
// fill="#3EAAAF"
// textAnchor={x > cx ? "start" : "end"}
// dominantBaseline="top"
// fontSize={10}
// >
// {data.values[index].name} ({value})
// </text>
// );
// }}
>
{data.values.map((entry, index) => (
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
))}
</Pie>
<Tooltip {...Styles.tooltip} />
</PieChart>
</ResponsiveContainer>
</ResponsiveContainer>
<div className="text-sm color-gray-medium">Top 5 </div>
</NoContent>
</div>
)
}

View file

@ -1,6 +1,9 @@
import React from 'react'
import { Table } from '../../common';
import { List } from 'immutable';
import { FilterKey } from 'Types/filter/filterType';
import { filtersMap } from 'Types/filter/newFilter';
import { NoContent } from 'UI';
const cols = [
{
@ -18,20 +21,40 @@ const cols = [
];
interface Props {
metric?: any,
data: any;
onClick?: (event, index) => void;
onClick?: (filters) => void;
}
function CustomMetriTable(props: Props) {
const { data = { values: [] }, onClick = () => null } = props;
const { metric = {}, data = { values: [] }, onClick = () => null } = props;
const rows = List(data.values);
const onClickHandler = (event, data) => {
const filters = Array<any>();
let filter = { ...filtersMap[metric.metricOf] }
filter.value = [data.name]
filter.type = filter.key
delete filter.key
delete filter.operatorOptions
delete filter.category
delete filter.icon
delete filter.label
delete filter.options
filters.push(filter);
onClick(filters);
}
return (
<div className="" style={{ height: '240px'}}>
<Table
small
cols={ cols }
rows={ rows }
rowClass="group"
/>
<NoContent show={data.values.length === 0} size="small">
<Table
small
cols={ cols }
rows={ rows }
rowClass="group"
onRowClick={ onClickHandler }
/>
</NoContent>
</div>
)
}

View file

@ -76,6 +76,19 @@ function CustomMetricWidget(props: Props) {
}).finally(() => setLoading(false));
}, [period])
const clickHandlerTable = (filters) => {
const activeWidget = {
widget: metric,
period: period,
...period.toTimestamps(),
filters,
// timestamp: payload.timestamp,
// index,
}
props.setActiveWidget(activeWidget);
// props.updateActiveState(metric.metricId, data);
}
const clickHandler = (event, index) => {
if (event) {
const payload = event.activePayload[0].payload;
@ -148,10 +161,11 @@ function CustomMetricWidget(props: Props) {
{metric.viewType === 'table' && (
<CustomMetricTable
metric={ metric }
data={ data[0] }
// params={ params }
// colors={ colors }
onClick={ clickHandler }
onClick={ clickHandlerTable }
/>
)}
</>

View file

@ -10,4 +10,5 @@
width: 70%;
margin: 0 auto;
background-color: white;
min-height: 220px;
}

View file

@ -18,6 +18,7 @@ export default class Table extends React.PureComponent {
small = false,
compare = false,
maxHeight = 200,
onRowClick = () => {},
} = this.props;
const { showAll } = this.state;
@ -33,7 +34,11 @@ export default class Table extends React.PureComponent {
</div>
<div className={ cn(stl.content, "thin-scrollbar") } style={{ maxHeight: maxHeight + 'px'}}>
{ rows.take(showAll ? 10 : (small ? 3 : 5)).map(row => (
<div className={ cn(rowClass, stl.row, { [stl.small]: small}) } key={ row.key }>
<div
className={ cn(rowClass, stl.row, { [stl.small]: small}) }
key={ row.key }
onClick={(e) => onRowClick(e, row)}
>
{ cols.map(({ cellClass = '', className = '', Component, key, toText = t => t, width }) => (
<div className={ cn(stl.cell, cellClass) } style={{ width }} key={ key }> { Component
? <Component compare={compare} data={ row } { ...rowProps } />

View file

@ -9,6 +9,7 @@ import { toast } from 'react-toastify';
import cn from 'classnames';
import DropdownPlain from '../../DropdownPlain';
import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
interface Props {
metric: any;
editMetric: (metric, shouldFetch?) => void;
@ -23,11 +24,11 @@ interface Props {
function CustomMetricForm(props: Props) {
const { metric, loading } = props;
// const metricOfOptions = metricOf.filter(i => i.key === metric.metricType);
const timeseriesOptions = metricOf.filter(i => i.key === 'timeseries');
const tableOptions = metricOf.filter(i => i.key === 'table');
const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries');
const tableOptions = metricOf.filter(i => i.type === 'table');
const isTable = metric.metricType === 'table';
const isTimeSeries = metric.metricType === 'timeseries';
const _issueOptions = [{ text: 'All', value: '' }].concat(issueOptions);
const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions);
const addSeries = () => {
@ -47,8 +48,8 @@ function CustomMetricForm(props: Props) {
}
if (name === 'metricOf') {
if (value === 'ISSUES') {
props.editMetric({ metricValue: [''] }, false);
if (value === FilterKey.ISSUE) {
props.editMetric({ metricValue: ['all'] }, false);
}
}
@ -139,7 +140,7 @@ function CustomMetricForm(props: Props) {
</>
)}
{metric.metricOf === 'ISSUES' && (
{metric.metricOf === FilterKey.ISSUE && (
<>
<span className="mx-3">issue type</span>
<DropdownPlain
@ -179,6 +180,10 @@ function CustomMetricForm(props: Props) {
series={series}
onRemoveSeries={() => removeSeries(index)}
canDelete={metric.series.size > 1}
emptyMessage={isTable ?
'Filter table data by user environment and metadata attributes. Use add step button below to filter.' :
'Add user event or filter to define the series by clicking Add Step.'
}
/>
</div>
))}

View file

@ -23,7 +23,8 @@ function SessionListModal(props: Props) {
props.fetchSessionList({
metricId: activeWidget.widget.metricId,
startDate: activeWidget.startTimestamp,
endDate: activeWidget.endTimestamp
endDate: activeWidget.endTimestamp,
filters: activeWidget.filters || [],
});
}, [activeWidget]);

View file

@ -1,5 +1,5 @@
.button {
padding: 0 8px;
padding: 0 4px;
border-radius: 3px;
color: $teal;
cursor: pointer;
@ -12,7 +12,7 @@
}
.dropdownTrigger {
padding: 4px 6px;
padding: 4px;
&:hover {
background-color: $gray-light;
}
@ -42,7 +42,7 @@
.dropdown {
display: flex !important;
padding: 4px 6px;
padding: 4px 4px;
border-radius: 3px;
color: $gray-darkest;
font-weight: 500;
@ -52,7 +52,7 @@
}
.dropdownTrigger {
padding: 4px 8px;
padding: 4px 4px;
border-radius: 3px;
&:hover {
background-color: $gray-light;

View file

@ -1,6 +1,6 @@
.dropdown {
display: flex !important;
padding: 4px 6px;
padding: 4px;
border-radius: 3px;
color: $gray-darkest;
font-weight: 500;
@ -12,7 +12,7 @@
}
.dropdownTrigger {
padding: 4px 8px;
padding: 4px;
border-radius: 3px;
&:hover {
background-color: $gray-light;

View file

@ -1,3 +1,5 @@
import { FilterKey } from 'Types/filter/filterType';
export const options = [
{ key: 'on', text: 'on', value: 'on' },
{ key: 'notOn', text: 'not on', value: 'notOn' },
@ -60,13 +62,13 @@ export const metricTypes = [
];
export const metricOf = [
{ text: 'Session Count', value: 'sessionCount', key: 'timeseries' },
{ text: 'Users', value: 'USERID', key: 'table' },
{ text: 'Issues', value: 'ISSUES', key: 'table' },
{ text: 'Browser', value: 'USERBROWSER', key: 'table' },
{ text: 'Device', value: 'USERDEVICE', key: 'table' },
{ text: 'Country', value: 'USERCOUNTRY', key: 'table' },
{ text: 'URL', value: 'VISITED_URL', key: 'table' },
{ text: 'Session Count', value: 'sessionCount', type: 'timeseries' },
{ text: 'Users', value: FilterKey.USERID, type: 'table' },
{ text: 'Issues', value: FilterKey.ISSUE, type: 'table' },
{ text: 'Browser', value: FilterKey.USER_BROWSER, type: 'table' },
{ text: 'Device', value: FilterKey.USER_DEVICE, type: 'table' },
{ text: 'Country', value: FilterKey.USER_COUNTRY, type: 'table' },
{ text: 'URL', value: FilterKey.LOCATION, type: 'table' },
]
export const issueOptions = [

View file

@ -3,6 +3,7 @@ import { List } from 'immutable';
import Filter from 'Types/filter';
import { validateName } from 'App/validate';
import { LAST_7_DAYS } from 'Types/app/period';
import { FilterKey } from 'Types/filter/filterType';
import { filterMap } from 'Duck/search';
export const FilterSeries = Record({
@ -47,11 +48,13 @@ export default Record({
toSaveData() {
const js = this.toJS();
js.metricValue = js.metricValue.map(value => value === 'all' ? '' : value);
js.series = js.series.map(series => {
series.filter.filters = series.filter.filters.map(filterMap);
// delete series._key
// delete series.key
delete series.key
return series;
});
@ -65,8 +68,10 @@ export default Record({
return js;
},
},
fromJS: ({ series, ...rest }) => ({
fromJS: ({ metricOf, metricValue, series, ...rest }) => ({
...rest,
series: List(series).map(FilterSeries),
metricOf,
metricValue: metricOf === FilterKey.ISSUE && metricValue.length === 0 ? ['all'] : metricValue,
}),
});