feat(ui) - custom metrics - wip
This commit is contained in:
parent
a030b2b955
commit
1d957f90b5
11 changed files with 194 additions and 160 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@
|
|||
width: 70%;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
|
@ -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 } />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue