feat(ui) - custom metrics - wip
This commit is contained in:
parent
0791744de7
commit
82e572b9d1
21 changed files with 266 additions and 72 deletions
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react'
|
||||
import { Styles } from '../../common';
|
||||
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||
import { LineChart, Line, Legend } from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
params: any;
|
||||
seriesMap: any;
|
||||
colors: any;
|
||||
}
|
||||
function CustomMetriLineChart(props: Props) {
|
||||
const { data, params, seriesMap, colors } = props;
|
||||
return (
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
data={ data }
|
||||
margin={Styles.chartMargins}
|
||||
// syncId={ showSync ? "domainsErrors_4xx" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: "Number of Sessions"
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{ seriesMap.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
name={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.6 }
|
||||
// fill="url(#colorCount)"
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomMetriLineChart
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetriLineChart';
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
}
|
||||
function CustomMetriPercentage(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center" style={{ height: '240px'}}>
|
||||
<div className="text-6xl">0%</div>
|
||||
<div className="text-lg mt-6">0 ( 0.0% ) from previous hour</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomMetriPercentage;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetriPercentage';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.wrapper {
|
||||
background-color: white;
|
||||
/* border: solid thin $gray-medium; */
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
}
|
||||
function CustomMetricPieChart(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center" style={{ height: '240px'}}>
|
||||
<div className="text-6xl">0%</div>
|
||||
<div className="text-lg mt-6">0 ( 0.0% ) from previous hour</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomMetricPieChart;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricPieChart';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.wrapper {
|
||||
background-color: white;
|
||||
/* border: solid thin $gray-medium; */
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react'
|
||||
import { Table } from '../../common';
|
||||
import { List } from 'immutable';
|
||||
|
||||
const cols = [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Resource',
|
||||
toText: name => name,
|
||||
width: '70%',
|
||||
},
|
||||
{
|
||||
key: 'sessions',
|
||||
title: 'Sessions',
|
||||
toText: sessions => sessions,
|
||||
width: '30%',
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
}
|
||||
function CustomMetriTable(props: Props) {
|
||||
const { data } = props;
|
||||
const rows = List([
|
||||
{ name: 'one', sessions: 2 },
|
||||
{ name: 'two', sessions: 3 },
|
||||
{ name: 'three', sessions: 4 },
|
||||
{ name: 'four', sessions: 1 },
|
||||
{ name: 'five', sessions: 6 },
|
||||
])
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center" style={{ height: '240px'}}>
|
||||
<Table
|
||||
small
|
||||
cols={ cols }
|
||||
rows={ rows }
|
||||
rowClass="group"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomMetriTable;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricTable';
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Loader, NoContent, Icon } from 'UI';
|
||||
import { Loader, NoContent, SegmentSelection, Icon } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip, LineChart, Line, Legend } from 'recharts';
|
||||
import Period, { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||
|
|
@ -9,6 +9,9 @@ import { getChartFormatter } from 'Types/dashboard/helper';
|
|||
import { remove } from 'Duck/customMetrics';
|
||||
import DateRange from 'Shared/DateRange';
|
||||
import { edit } from 'Duck/customMetrics';
|
||||
import CustomMetriLineChart from '../CustomMetriLineChart';
|
||||
import CustomMetriPercentage from '../CustomMetriPercentage';
|
||||
import CustomMetricTable from '../CustomMetricTable';
|
||||
|
||||
import APIClient from 'App/api_client';
|
||||
|
||||
|
|
@ -83,11 +86,28 @@ function CustomMetricWidget(props: Props) {
|
|||
props.edit({ ...changedDates, rangeName: changedDates.rangeValue });
|
||||
}
|
||||
|
||||
const chagneViewType = (e, { name, value }) => {
|
||||
props.edit({ [ name ]: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-10">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="mr-auto font-medium">Preview</div>
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<SegmentSelection
|
||||
name="viewType"
|
||||
className="my-3"
|
||||
size="extraSmall"
|
||||
onSelect={ chagneViewType }
|
||||
value={{ value: metric.viewType }}
|
||||
list={ [
|
||||
{ value: 'chart', icon: 'graph-up-arrow' },
|
||||
{ value: 'percent', icon: 'hash' },
|
||||
]}
|
||||
/>
|
||||
<div className="mx-2" />
|
||||
<span className="mr-1 color-gray-medium">Time Range</span>
|
||||
<DateRange
|
||||
rangeValue={metric.rangeName}
|
||||
startDate={metric.startDate}
|
||||
|
|
@ -105,44 +125,27 @@ function CustomMetricWidget(props: Props) {
|
|||
size="small"
|
||||
show={ data.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
{ metric.metricType === 'timeseries' && (
|
||||
<>
|
||||
{ metric.viewType === 'percent' && (
|
||||
<CustomMetriPercentage data={data} />
|
||||
)}
|
||||
{ metric.viewType === 'chart' && (
|
||||
<CustomMetriLineChart
|
||||
data={data}
|
||||
margin={Styles.chartMargins}
|
||||
syncId={ showSync ? "domainsErrors_4xx" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
interval={params.density/7}
|
||||
seriesMap={seriesMap}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: "Number of Sessions"
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{ seriesMap.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
name={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.6 }
|
||||
// fill="url(#colorCount)"
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{ metric.metricType === 'table' && (
|
||||
<div className="p-3">
|
||||
<CustomMetricTable data={data} />
|
||||
</div>
|
||||
)}
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ export default class Table extends React.PureComponent {
|
|||
rowProps,
|
||||
rowClass = '',
|
||||
small = false,
|
||||
compare = false
|
||||
compare = false,
|
||||
maxHeight = 200,
|
||||
} = this.props;
|
||||
const { showAll } = this.state;
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ export default class Table extends React.PureComponent {
|
|||
<div key={ key } style={ { width } } className={ stl.header }>{ title }</div>)
|
||||
}
|
||||
</div>
|
||||
<div className={ cn(stl.content, "thin-scrollbar") }>
|
||||
<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 }>
|
||||
{ cols.map(({ cellClass = '', className = '', Component, key, toText = t => t, width }) => (
|
||||
|
|
@ -42,7 +43,7 @@ export default class Table extends React.PureComponent {
|
|||
)) }
|
||||
</div>
|
||||
)) }
|
||||
|
||||
</div>
|
||||
{ rows.size > (small ? 3 : 5) && !showAll &&
|
||||
<div className="w-full flex justify-center mt-3">
|
||||
<Button
|
||||
|
|
@ -56,7 +57,6 @@ export default class Table extends React.PureComponent {
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ 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 addSeries = () => {
|
||||
props.addSeries();
|
||||
|
|
@ -34,7 +36,17 @@ function CustomMetricForm(props: Props) {
|
|||
}
|
||||
|
||||
const write = ({ target: { value, name } }) => props.editMetric({ [ name ]: value }, false);
|
||||
const writeOption = (e, { value, name }) => props.editMetric({ [ name ]: value }, false);
|
||||
const writeOption = (e, { value, name }) => {
|
||||
props.editMetric({ [ name ]: value }, false);
|
||||
|
||||
if (name === 'metricType') {
|
||||
if (value === 'timeseries') {
|
||||
props.editMetric({ metricOf: timeseriesOptions[0].value }, false);
|
||||
} else if (value === 'table') {
|
||||
props.editMetric({ metricOf: tableOptions[0].value }, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const changeConditionTab = (e, { name, value }) => {
|
||||
props.editMetric({[ 'viewType' ]: value });
|
||||
|
|
@ -89,22 +101,44 @@ function CustomMetricForm(props: Props) {
|
|||
value={ metric.metricType }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
|
||||
{metric.metricType === 'timeseries' && (
|
||||
<>
|
||||
<span className="mx-3">of</span>
|
||||
<DropdownPlain
|
||||
name="metricOf"
|
||||
options={metricOfOptions}
|
||||
options={timeseriesOptions}
|
||||
value={ metric.metricOf }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{metric.metricType === 'table' && (
|
||||
<>
|
||||
<span className="mx-3">of</span>
|
||||
<DropdownPlain
|
||||
name="metricOf"
|
||||
options={tableOptions}
|
||||
value={ metric.metricOf }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{metric.metricType === 'table' && (
|
||||
<>
|
||||
<span className="mx-3">showing</span>
|
||||
<DropdownPlain
|
||||
name="viewType"
|
||||
options={[
|
||||
{ value: 'sessionCount', text: 'Session Count' },
|
||||
]}
|
||||
value={ metric.metricType }
|
||||
value={ metric.viewType }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* <div className="flex items-center">
|
||||
<span className="bg-white p-1 px-2 border rounded" style={{ height: '30px'}}>Timeseries</span>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
border-radius: 3px;
|
||||
color: $gray-darkest;
|
||||
font-weight: 500;
|
||||
background-color: white;
|
||||
border: solid thin $gray-light;
|
||||
&:hover {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export default function DropdownPlain(props: Props) {
|
||||
const { name = "sort", value, options, icon = "chevron-down", direction = "left" } = props;
|
||||
const { name = "sort", value, options, icon = "chevron-down", direction = "right" } = props;
|
||||
return (
|
||||
<div>
|
||||
<Dropdown
|
||||
|
|
@ -22,7 +22,7 @@ export default function DropdownPlain(props: Props) {
|
|||
direction={direction}
|
||||
options={ options }
|
||||
onChange={ props.onChange }
|
||||
floating
|
||||
// floating
|
||||
scrolling
|
||||
selectOnBlur={ false }
|
||||
// defaultValue={ value }
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class SegmentSelection extends React.Component {
|
|||
<div className={ cn(styles.wrapper, {
|
||||
[styles.primary] : primary,
|
||||
[styles.small] : size === 'small' || small,
|
||||
[styles.extraSmall] : extraSmall,
|
||||
[styles.extraSmall] : size === 'extraSmall' || extraSmall,
|
||||
}, className) }
|
||||
>
|
||||
{ list.map(item => (
|
||||
|
|
@ -27,7 +27,7 @@ class SegmentSelection extends React.Component {
|
|||
data-active={ this.props.value && this.props.value.value === item.value }
|
||||
onClick={ () => !item.disabled && this.setActiveItem(item) }
|
||||
>
|
||||
{ item.icon && <Icon name={ item.icon } size="20" marginRight="10" /> }
|
||||
{ item.icon && <Icon name={ item.icon } size={size === "extraSmall" ? 12 : 20} marginRight={ item.name ? "10" : "" } /> }
|
||||
<div>{ item.name }</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@
|
|||
padding: 10px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
border-right: solid thin $teal;
|
||||
cursor: pointer;
|
||||
background-color: $gray-lightest;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
border-right: solid thin $gray-light;
|
||||
|
||||
& span svg {
|
||||
fill: $gray-medium;
|
||||
|
|
@ -53,6 +53,7 @@
|
|||
& .item {
|
||||
color: $teal;
|
||||
background-color: white;
|
||||
border-right: solid thin $teal;
|
||||
&[data-active=true] {
|
||||
background-color: $teal;
|
||||
color: white;
|
||||
|
|
@ -65,6 +66,6 @@
|
|||
}
|
||||
|
||||
.extraSmall .item {
|
||||
padding: 0 4px;
|
||||
padding: 6px !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ export const metricTypes = [
|
|||
|
||||
export const metricOf = [
|
||||
{ text: 'Session Count', value: 'sessionCount', key: 'timeseries' },
|
||||
{ text: 'Users', value: 'users', key: 'table' },
|
||||
{ text: 'Users', value: 'USERID', key: 'table' },
|
||||
{ text: 'Rage Click', value: 'rageClick', key: 'table' },
|
||||
{ text: 'Dead Click', value: 'deadClick', key: 'table' },
|
||||
{ text: 'Browser', value: 'browser', key: 'table' },
|
||||
|
|
|
|||
3
frontend/app/svg/icons/graph-up-arrow.svg
Normal file
3
frontend/app/svg/icons/graph-up-arrow.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-graph-up-arrow" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M0 0h1v15h15v1H0V0Zm10 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V4.9l-3.613 4.417a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61L13.445 4H10.5a.5.5 0 0 1-.5-.5Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 402 B |
3
frontend/app/svg/icons/hash.svg
Normal file
3
frontend/app/svg/icons/hash.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hash" viewBox="0 0 16 16">
|
||||
<path d="M8.39 12.648a1.32 1.32 0 0 0-.015.18c0 .305.21.508.5.508.266 0 .492-.172.555-.477l.554-2.703h1.204c.421 0 .617-.234.617-.547 0-.312-.188-.53-.617-.53h-.985l.516-2.524h1.265c.43 0 .618-.227.618-.547 0-.313-.188-.524-.618-.524h-1.046l.476-2.304a1.06 1.06 0 0 0 .016-.164.51.51 0 0 0-.516-.516.54.54 0 0 0-.539.43l-.523 2.554H7.617l.477-2.304c.008-.04.015-.118.015-.164a.512.512 0 0 0-.523-.516.539.539 0 0 0-.531.43L6.53 5.484H5.414c-.43 0-.617.22-.617.532 0 .312.187.539.617.539h.906l-.515 2.523H4.609c-.421 0-.609.219-.609.531 0 .313.188.547.61.547h.976l-.516 2.492c-.008.04-.015.125-.015.18 0 .305.21.508.5.508.265 0 .492-.172.554-.477l.555-2.703h2.242l-.515 2.492zm-1-6.109h2.266l-.515 2.563H6.859l.532-2.563z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 855 B |
|
|
@ -27,9 +27,9 @@ export const FilterSeries = Record({
|
|||
export default Record({
|
||||
metricId: undefined,
|
||||
name: 'Series',
|
||||
metricType: 'timeseries',
|
||||
metricOf: 'sessionCount',
|
||||
viewType: 'sessionCount',
|
||||
metricType: 'table',
|
||||
metricOf: 'USERID',
|
||||
viewType: 'lineChart',
|
||||
series: List(),
|
||||
isPublic: true,
|
||||
startDate: '',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue