feat(ui) - custom metrics
This commit is contained in:
parent
155e7e4331
commit
e80293efa4
26 changed files with 365 additions and 148 deletions
|
|
@ -141,21 +141,21 @@ export default class EventFilter extends React.PureComponent {
|
|||
{ hasFilters &&
|
||||
<div className={cn("bg-white rounded border-gray-light mt-2 relative", { 'blink-border' : blink })}>
|
||||
<div className="absolute right-0 top-0 m-3 z-10 flex items-center">
|
||||
<div className="mr-2">Operator</div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="condition"
|
||||
extraSmall={true}
|
||||
// className="my-3"
|
||||
onSelect={ this.changeConditionTab }
|
||||
value={{ value: appliedFilter.condition }}
|
||||
list={ [
|
||||
{ name: 'AND', value: 'and' },
|
||||
{ name: 'OR', value: 'or' },
|
||||
{ name: 'THEN', value: 'then' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="mr-2">Operator</div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="condition"
|
||||
extraSmall={true}
|
||||
// className="my-3"
|
||||
onSelect={ this.changeConditionTab }
|
||||
value={{ value: appliedFilter.condition }}
|
||||
list={ [
|
||||
{ name: 'AND', value: 'and' },
|
||||
{ name: 'OR', value: 'or' },
|
||||
{ name: 'THEN', value: 'then' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ events.size > 0 &&
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import SideMenuSection from './SideMenu/SideMenuSection';
|
|||
import styles from './dashboard.css';
|
||||
import WidgetSection from 'Shared/WidgetSection/WidgetSection';
|
||||
import OverviewWidgets from './Widgets/OverviewWidgets/OverviewWidgets';
|
||||
import CustomMetricsWidgets from './Widgets/CustomMetricsWidgets/CustomMetricsWidgets';
|
||||
import WidgetHolder from './WidgetHolder/WidgetHolder';
|
||||
import MetricsFilters from 'Shared/MetricsFilters/MetricsFilters';
|
||||
import { withRouter } from 'react-router';
|
||||
|
|
@ -47,6 +48,7 @@ const OVERVIEW = 'overview';
|
|||
const PERFORMANCE = 'performance';
|
||||
const ERRORS_N_CRASHES = 'errors_n_crashes';
|
||||
const RESOURCES = 'resources';
|
||||
const CUSTOM_METRICS = 'custom_metrics';
|
||||
|
||||
const menuList = [
|
||||
{
|
||||
|
|
@ -201,6 +203,12 @@ export default class Dashboard extends React.PureComponent {
|
|||
</div>
|
||||
</WidgetSection>
|
||||
|
||||
<WidgetSection title="Custom Metrics" type="customMetrics" className="mb-4">
|
||||
<div className={ cn("gap-4", { 'grid grid-cols-2' : !comparing })} ref={this.list[CUSTOM_METRICS]}>
|
||||
<CustomMetricsWidgets />
|
||||
</div>
|
||||
</WidgetSection>
|
||||
|
||||
<WidgetSection title="Errors" className="mb-4" type="errors">
|
||||
<div className={ cn("gap-4", { 'grid grid-cols-2' : !comparing })} ref={this.list[ERRORS_N_CRASHES]}>
|
||||
{ dashboardAppearance.impactedSessionsByJsErrors && <WidgetHolder Component={SessionsAffectedByJSErrors} /> }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { widgetHOC, Styles } from '../../common';
|
||||
import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||
import CustomMetricWidgetHoc from '../../common/CustomMetricWidgetHoc';
|
||||
|
||||
const customParams = rangeName => {
|
||||
const params = { density: 70 }
|
||||
|
||||
if (rangeName === LAST_24_HOURS) params.density = 70
|
||||
if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||
if (rangeName === YESTERDAY) params.density = 70
|
||||
if (rangeName === LAST_7_DAYS) params.density = 70
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
interface Period {
|
||||
rangeName: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
widget: any;
|
||||
loading?: boolean;
|
||||
data?: any;
|
||||
showSync?: boolean;
|
||||
compare?: boolean;
|
||||
period?: Period;
|
||||
}
|
||||
function CustomMetricWidget(props: Props) {
|
||||
const { widget, loading = false, data = { chart: []}, showSync, compare, period = { rangeName: ''} } = props;
|
||||
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
const params = customParams(period.rangeName)
|
||||
const gradientDef = Styles.gradientDef();
|
||||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={Styles.chartMargins}
|
||||
syncId={ showSync ? "impactedSessionsBySlowPages" : undefined }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density/7} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Requests" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Sessions"
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke={colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={compare ? 'url(#colorCountCompare)' : 'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricWidgetHoc(CustomMetricWidget);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricWidget';
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchList } from 'Duck/customMetrics';
|
||||
import { list } from 'App/components/BugFinder/CustomFilters/filterModal.css';
|
||||
import CustomMetricWidget from './CustomMetricWidget';
|
||||
|
||||
interface Props {
|
||||
fetchList: Function;
|
||||
list: any;
|
||||
}
|
||||
function CustomMetricsWidgets(props: Props) {
|
||||
const { list } = props;
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchList()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.map((item: any) => (
|
||||
<CustomMetricWidget widget={item} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
list: state.getIn(['customMetrics', 'list']),
|
||||
}), { fetchList })(CustomMetricsWidgets);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricsWidgets';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.wrapper {
|
||||
background-color: white;
|
||||
/* border: solid thin $gray-medium; */
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import stl from './CustomMetricWidgetHoc.css';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
interface Props {
|
||||
}
|
||||
const CustomMetricWidgetHoc = ({ ...rest }: Props) => BaseComponent => {
|
||||
|
||||
console.log('CustomMetricWidgetHoc', rest);
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<div className="flex items-center mb-10 p-2">
|
||||
<div className="font-medium">Widget Name</div>
|
||||
<div className="ml-auto">
|
||||
<div className="cursor-pointer">
|
||||
<Icon name="bell-plus" size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <BaseComponent {...rest} /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricWidgetHoc;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricWidgetHoc';
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Form, SegmentSelection, Button } from 'UI';
|
||||
import { Form, SegmentSelection, Button, IconButton } from 'UI';
|
||||
import FilterSeries from '../FilterSeries';
|
||||
import { connect } from 'react-redux';
|
||||
import { edit as editMetric, save } from 'Duck/customMetrics';
|
||||
|
|
@ -68,13 +68,13 @@ function CustomMetricForm(props: Props) {
|
|||
<div className="form-group">
|
||||
<label className="font-medium">Metric Type</label>
|
||||
<div className="flex items-center">
|
||||
<span>Timeseries</span>
|
||||
<span className="mx-2">of</span>
|
||||
<div style={{ width: "250px"}}>
|
||||
<span className="bg-white p-1 px-2 border rounded">Timeseries</span>
|
||||
<span className="mx-2 color-gray-medium">of</span>
|
||||
<div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="condition"
|
||||
extraSmall={true}
|
||||
small={true}
|
||||
// className="my-3"
|
||||
onSelect={ changeConditionTab }
|
||||
value={{ value: metric.type }}
|
||||
|
|
@ -88,7 +88,7 @@ function CustomMetricForm(props: Props) {
|
|||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="font-medium">Sereis</label>
|
||||
<label className="font-medium">Series</label>
|
||||
{metric.series && metric.series.size > 0 && metric.series.map((series: any, index: number) => (
|
||||
<div className="mb-2">
|
||||
<FilterSeries
|
||||
|
|
@ -100,7 +100,9 @@ function CustomMetricForm(props: Props) {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<Button onClick={addSeries}>Add Series</Button>
|
||||
<div className="flex justify-end">
|
||||
<IconButton onClick={addSeries} primaryText label="SERIES" icon="plus" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute w-full bottom-0 px-5 py-2 bg-white">
|
||||
|
|
|
|||
|
|
@ -1,38 +1,29 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { IconButton, SlideModal } from 'UI'
|
||||
import CustomMetricForm from './CustomMetricForm';
|
||||
|
||||
interface Props {}
|
||||
function CustomMetrics(props: Props) {
|
||||
const [showModal, setShowModal] = useState(true);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton outline icon="plus" label="CREATE METRIC" />
|
||||
<div className="self-start">
|
||||
<IconButton outline icon="plus" label="CREATE METRIC" onClick={() => setShowModal(true)} />
|
||||
|
||||
<SlideModal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3">{ 'Custom Metric' }</span>
|
||||
{/* <IconButton
|
||||
circle
|
||||
size="small"
|
||||
icon="plus"
|
||||
outline
|
||||
id="add-button"
|
||||
// onClick={ () => toggleForm({}, true) }
|
||||
/> */}
|
||||
</div>
|
||||
}
|
||||
isDisplayed={ true }
|
||||
// onClose={ () => {
|
||||
// toggleForm({}, false);
|
||||
// setShowAlerts(false);
|
||||
// } }
|
||||
isDisplayed={ showModal }
|
||||
onClose={ () => setShowModal(false)}
|
||||
// size="medium"
|
||||
content={
|
||||
<div className="bg-gray-light-shade">
|
||||
content={ showModal && (
|
||||
<div style={{ backgroundColor: '#f6f6f6'}}>
|
||||
<CustomMetricForm />
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||
import FilterList from 'Shared/Filters/FilterList';
|
||||
import { edit, updateSeries } from 'Duck/customMetrics';
|
||||
import { connect } from 'react-redux';
|
||||
import { IconButton, Button, Icon } from 'UI';
|
||||
import { IconButton, Button, Icon, SegmentSelection } from 'UI';
|
||||
import FilterSelection from '../../Filters/FilterSelection';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -14,6 +14,7 @@ interface Props {
|
|||
}
|
||||
|
||||
function FilterSeries(props: Props) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const { series, seriesIndex } = props;
|
||||
|
||||
const onAddFilter = (filter) => {
|
||||
|
|
@ -46,6 +47,16 @@ function FilterSeries(props: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
const onChangeEventsOrder = (e, { name, value }) => {
|
||||
props.updateSeries(seriesIndex, {
|
||||
...series.toData(),
|
||||
filter: {
|
||||
...series.filter,
|
||||
eventsOrder: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onRemoveFilter = (filterIndex) => {
|
||||
const newFilters = series.filter.filters.filter((_filter, i) => {
|
||||
return i !== filterIndex;
|
||||
|
|
@ -62,35 +73,43 @@ function FilterSeries(props: Props) {
|
|||
|
||||
return (
|
||||
<div className="border rounded bg-white">
|
||||
<div className="border-b px-5 h-12 flex items-center">
|
||||
<span className="mr-auto">{ series.name }</span>
|
||||
<div className="flex items-center cursor-pointer" onClick={props.onRemoveSeries}>
|
||||
<Icon name="trash" size="16" />
|
||||
<div className="border-b px-5 h-12 flex items-center relative">
|
||||
<div className="font-medium">{ series.name }</div>
|
||||
|
||||
<div className="flex items-center cursor-pointer ml-auto" >
|
||||
<div onClick={props.onRemoveSeries} className="ml-3">
|
||||
<Icon name="trash" size="16" />
|
||||
</div>
|
||||
|
||||
<div onClick={() => setExpanded(!expanded)} className="ml-3">
|
||||
<Icon name="chevron-down" size="16" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
{ series.filter.filters.size > 0 ? (
|
||||
<FilterList
|
||||
filters={series.filter.filters.toJS()}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
/>
|
||||
): (
|
||||
<div>Add user event or filter to build the series.</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-5 border-t h-12 flex items-center">
|
||||
<FilterSelection
|
||||
filter={undefined}
|
||||
onFilterClick={onAddFilter}
|
||||
>
|
||||
{/* <Button className="flex items-center">
|
||||
<Icon name="plus" size="16" />
|
||||
<span>Add Step</span>
|
||||
</Button> */}
|
||||
<IconButton primaryText label="ADD STEP" icon="plus" />
|
||||
</FilterSelection>
|
||||
</div>
|
||||
{ expanded && (
|
||||
<>
|
||||
<div className="p-5">
|
||||
{ series.filter.filters.size > 0 ? (
|
||||
<FilterList
|
||||
filters={series.filter.filters.toJS()}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
/>
|
||||
): (
|
||||
<div className="color-gray-medium">Add user event or filter to build the series.</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-5 border-t h-12 flex items-center">
|
||||
<FilterSelection
|
||||
filter={undefined}
|
||||
onFilterClick={onAddFilter}
|
||||
>
|
||||
<IconButton primaryText label="ADD STEP" icon="plus" />
|
||||
</FilterSelection>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,20 +16,36 @@
|
|||
& .right {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
background-color: $gray-lightest;
|
||||
border-left: solid thin $gray-light !important;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
& div {
|
||||
/* background-color: red; */
|
||||
border-left: solid thin $gray-light !important;
|
||||
width: 28px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:last-child {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
border-radius: 0 0 3px 3px;
|
||||
box-shadow: 0 2px 10px 0 $gray-light;
|
||||
padding: 20px;
|
||||
border: solid thin $gray-light !important;
|
||||
/* box-shadow: 0 2px 10px 0 $gray-light; */
|
||||
/* padding: 20px; */
|
||||
background-color: white;
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
|
|
@ -43,7 +59,7 @@
|
|||
.filterItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: all 0.4s;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const hiddenStyle = {
|
|||
|
||||
interface Props {
|
||||
showOrButton?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
onRemoveValue?: () => void;
|
||||
onAddValue?: () => void;
|
||||
endpoint?: string;
|
||||
|
|
@ -25,6 +26,7 @@ interface Props {
|
|||
|
||||
function FilterAutoComplete(props: Props) {
|
||||
const {
|
||||
showCloseButton = false,
|
||||
placeholder = 'Type to search',
|
||||
method = 'GET',
|
||||
showOrButton = false,
|
||||
|
|
@ -94,7 +96,7 @@ function FilterAutoComplete(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative flex items-center">
|
||||
<div className={stl.wrapper}>
|
||||
<input
|
||||
name="query"
|
||||
|
|
@ -115,11 +117,13 @@ function FilterAutoComplete(props: Props) {
|
|||
className={stl.right}
|
||||
// onClick={showOrButton ? onRemoveValue : onAddValue}
|
||||
>
|
||||
{ !showOrButton && <Icon onClick={onRemoveValue} name="close" size="18" /> }
|
||||
{ showOrButton && <span onClick={onAddValue} className="px-1">or</span>}
|
||||
{ showCloseButton && <div onClick={onRemoveValue}><Icon name="close" size="18" /></div> }
|
||||
{ showOrButton && <div onClick={onAddValue} className="color-teal"><span className="px-1">or</span></div> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ !showOrButton && <div className="ml-3">or</div> }
|
||||
|
||||
{/* <textarea style={hiddenStyle} ref={(ref) => this.hiddenInput = ref }></textarea> */}
|
||||
|
||||
{ showModal && (options.length > 0 || loading) &&
|
||||
|
|
|
|||
|
|
@ -44,26 +44,29 @@ function FitlerItem(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center mb-3">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="flex items-start mr-auto">
|
||||
<div className="mt-1 w-6 h-6 text-xs flex justify-center rounded-full bg-gray-light-shade mr-2">{filterIndex+1}</div>
|
||||
<FilterSelection filter={filter} onFilterClick={replaceFilter} />
|
||||
<FilterOperator filter={filter} onChange={onOperatorChange} className="mx-2"/>
|
||||
<FilterOperator filter={filter} onChange={onOperatorChange} className="mx-2 flex-shrink-0"/>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{filter.value && filter.value.map((value, valueIndex) => (
|
||||
<FilterValue
|
||||
showCloseButton={filter.value.length > 1}
|
||||
showOrButton={valueIndex === filter.value.length - 1}
|
||||
key={valueIndex}
|
||||
// filter={filter}
|
||||
// key={valueIndex}
|
||||
value={value}
|
||||
key={filter.key}
|
||||
index={valueIndex}
|
||||
onAddValue={onAddValue}
|
||||
onRemoveValue={() => onRemoveValue(valueIndex)}
|
||||
onSelect={(e, item) => onSelect(e, valueIndex, item)}
|
||||
onSelect={(e, item) => onSelect(e, item, valueIndex)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="flex self-start mt-2">
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={props.onRemoveFilter}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState} from 'react';
|
||||
import FilterItem from '../FilterItem';
|
||||
import { SegmentSelection } from 'UI';
|
||||
|
||||
interface Props {
|
||||
filters: any[]; // event/filter
|
||||
|
|
@ -19,6 +20,27 @@ function FilterList(props: Props) {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="mb-2 text-sm color-gray-medium mr-auto">EVENTS</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2 color-gray-medium text-sm">Events Order</div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="eventsOrder"
|
||||
extraSmall={true}
|
||||
// className="my-3"
|
||||
// onSelect={onChangeEventsOrder }
|
||||
onSelect={() => null }
|
||||
// value={{ value: series.filter.eventsOrder }}
|
||||
value={{ value: 'and' }}
|
||||
list={ [
|
||||
{ name: 'AND', value: 'and' },
|
||||
{ name: 'OR', value: 'or' },
|
||||
{ name: 'THEN', value: 'then' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{filters.map((filter, filterIndex) => (
|
||||
<FilterItem
|
||||
filterIndex={filterIndex}
|
||||
|
|
@ -27,6 +49,16 @@ function FilterList(props: Props) {
|
|||
onRemoveFilter={() => onRemoveFilter(filterIndex) }
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* <div>Filters</div>
|
||||
{filters.filter(f => !f.isEvent).map((filter, filterIndex) => (
|
||||
<FilterItem
|
||||
filterIndex={filter.index}
|
||||
filter={filter}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex) }
|
||||
/>
|
||||
))} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ interface Props {
|
|||
function FilterModal(props: Props) {
|
||||
const { filters, onFilterClick = () => null } = props;
|
||||
return (
|
||||
<div className="border p-3" style={{ width: '560px', height: '400px', overflowY: 'auto'}}>
|
||||
<div className="border p-3" style={{ width: '490px', height: '400px', overflowY: 'auto'}}>
|
||||
<div className="grid grid-flow-row-dense grid-cols-2">
|
||||
{filters && Object.keys(filters).map((key) => (
|
||||
<div className="p-3">
|
||||
<div className="uppercase font-medium mb-1">{key}</div>
|
||||
<div>
|
||||
{filters[key].map((filter: any) => (
|
||||
<div className="flex items-center py-2 cursor-pointer" onClick={() => onFilterClick(filter)}>
|
||||
<div className="flex items-center py-2 cursor-pointer hover:bg-gray-lightest -mx-2 px-2" onClick={() => onFilterClick(filter)}>
|
||||
<Icon name={filter.icon} size="16"/>
|
||||
<span className="ml-2">{filter.label}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,19 +10,20 @@ interface Props {
|
|||
}
|
||||
function FilterSelection(props: Props) {
|
||||
const { filter, onFilterClick, children } = props;
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative flex-shrink-0">
|
||||
<OutsideClickDetectingDiv
|
||||
className="relative"
|
||||
onClickOutside={ () => setTimeout(function() {
|
||||
setShowModal(false)
|
||||
}, 20)}
|
||||
}, 50)}
|
||||
>
|
||||
{ children ? React.cloneElement(children, { onClick: () => setShowModal(true)}) : (
|
||||
<div
|
||||
className="rounded border py-1 px-3 flex items-center cursor-pointer bg-gray-lightest text-ellipsis"
|
||||
style={{ width: '140px', height: '30px'}}
|
||||
className="rounded py-1 px-3 flex items-center cursor-pointer bg-gray-lightest text-ellipsis"
|
||||
style={{ width: '140px', height: '30px', border: 'solid thin rgba(34, 36, 38, 0.15)'}}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<span className="mr-auto truncate">{filter.label}</span>
|
||||
|
|
|
|||
|
|
@ -4,25 +4,29 @@ import FilterAutoComplete from '../FilterAutoComplete';
|
|||
interface Props {
|
||||
index: number;
|
||||
value: any; // event/filter
|
||||
// type: string;
|
||||
key: string;
|
||||
onRemoveValue?: () => void;
|
||||
onAddValue?: () => void;
|
||||
showCloseButton: boolean;
|
||||
showOrButton: boolean;
|
||||
onSelect: (e, item) => void;
|
||||
}
|
||||
function FilterValue(props: Props) {
|
||||
const { index, value, showOrButton, onRemoveValue , onAddValue } = props;
|
||||
const { index, value, key, showOrButton, showCloseButton, onRemoveValue , onAddValue } = props;
|
||||
|
||||
return (
|
||||
<FilterAutoComplete
|
||||
value={value}
|
||||
showCloseButton={showCloseButton}
|
||||
showOrButton={showOrButton}
|
||||
onAddValue={onAddValue}
|
||||
onRemoveValue={onRemoveValue}
|
||||
method={'GET'}
|
||||
endpoint='/events/search'
|
||||
params={undefined}
|
||||
params={{ type: key }}
|
||||
headerText={''}
|
||||
placeholder={''}
|
||||
// placeholder={''}
|
||||
onSelect={props.onSelect}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
align-items: center;
|
||||
justify-content: space-around;
|
||||
border: solid thin $gray-light;
|
||||
border-radius: 5px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
|
||||
& .item {
|
||||
color: $gray-medium;
|
||||
font-weight: medium;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
/* flex: 1; */
|
||||
text-align: center;
|
||||
border-right: solid thin $gray-light;
|
||||
border-right: solid thin $teal;
|
||||
cursor: pointer;
|
||||
background-color: $gray-lightest;
|
||||
display: flex;
|
||||
|
|
@ -64,6 +64,6 @@
|
|||
}
|
||||
|
||||
.extraSmall .item {
|
||||
padding: 2px 4px;
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ function reducer(state = initialState, action = {}) {
|
|||
case EDIT:
|
||||
return state.mergeIn([ 'instance' ], CustomMetric(action.instance));
|
||||
case UPDATE_SERIES:
|
||||
console.log('update series', action.series);
|
||||
return state.setIn(['instance', 'series', action.index], FilterSeries(action.series));
|
||||
case success(SAVE):
|
||||
return state.set([ 'instance' ], CustomMetric(action.data));
|
||||
|
|
@ -55,11 +56,7 @@ function reducer(state = initialState, action = {}) {
|
|||
return state.set("instance", ErrorInfo(action.data));
|
||||
case success(FETCH_LIST):
|
||||
const { data } = action;
|
||||
return state
|
||||
.set("totalCount", data ? data.total : 0)
|
||||
.set("list", List(data && data.errors).map(CustomMetric)
|
||||
.filter(e => e.parentErrorId == null)
|
||||
.map(e => e.update("chart", chartWrapper)));
|
||||
return state.set("list", List(data.map(CustomMetric)));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
|
@ -95,11 +92,9 @@ export function save(instance) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchList(params = {}, clear = false) {
|
||||
export function fetchList() {
|
||||
return {
|
||||
types: array(FETCH_LIST),
|
||||
call: client => client.post('/errors/search', params),
|
||||
clear,
|
||||
params: cleanParams(params),
|
||||
call: client => client.get(`/${name}s`),
|
||||
};
|
||||
}
|
||||
|
|
@ -14,11 +14,9 @@ import { newFiltersList } from 'Types/filter'
|
|||
import NewFilter, { filtersMap } from 'Types/filter/newFilter';
|
||||
|
||||
const filterOptions = {}
|
||||
// newFiltersList.forEach(filter => {
|
||||
// filterOptions[filter.category] = filter
|
||||
// })
|
||||
|
||||
Object.keys(filtersMap).forEach(key => {
|
||||
// const filter = NewFilter(filtersMap[key]);
|
||||
const filter = filtersMap[key];
|
||||
if (filterOptions.hasOwnProperty(filter.category)) {
|
||||
filterOptions[filter.category].push(filter);
|
||||
|
|
|
|||
|
|
@ -107,8 +107,9 @@
|
|||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 25px;
|
||||
& label {
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ export const FilterSeries = Record({
|
|||
methods: {
|
||||
toData() {
|
||||
const js = this.toJS();
|
||||
delete js.key;
|
||||
// js.filter = js.filter.toData();
|
||||
return js;
|
||||
},
|
||||
|
|
@ -42,6 +43,7 @@ export default Record({
|
|||
|
||||
toData() {
|
||||
const js = this.toJS();
|
||||
|
||||
js.series = js.series.map(series => {
|
||||
series.filter.filters = series.filter.filters.map(filter => {
|
||||
delete filter.operatorOptions
|
||||
|
|
@ -51,6 +53,8 @@ export default Record({
|
|||
return series;
|
||||
});
|
||||
|
||||
delete js.key;
|
||||
|
||||
return js;
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export default Record({
|
|||
suspicious: undefined,
|
||||
consoleLevel: undefined,
|
||||
strict: false,
|
||||
eventsOrder: 'and',
|
||||
}, {
|
||||
idKey: 'searchId',
|
||||
methods: {
|
||||
|
|
@ -53,10 +54,12 @@ export default Record({
|
|||
const js = this.toJS();
|
||||
js.filters = js.filters.map(filter => {
|
||||
delete filter.operatorOptions
|
||||
delete filter._key
|
||||
return filter;
|
||||
});
|
||||
|
||||
delete js.createdAt;
|
||||
delete js.key;
|
||||
return js;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -219,41 +219,41 @@ export const booleanOptions = [
|
|||
]
|
||||
|
||||
export const filtersMap = {
|
||||
[TYPES.CLICK]: { category: 'interactions', label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.INPUT]: { category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.LOCATION]: { category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.CLICK]: { key: TYPES.CLICK, type: 'multiple', category: 'interactions', label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click', isEvent: true },
|
||||
[TYPES.INPUT]: { key: TYPES.INPUT, type: 'multiple', category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true },
|
||||
[TYPES.LOCATION]: { key: TYPES.LOCATION, type: 'multiple', category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true },
|
||||
|
||||
[TYPES.USER_OS]: { category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USER_BROWSER]: { category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USER_DEVICE]: { category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.PLATFORM]: { category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.REVID]: { category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USER_OS]: { key: TYPES.USER_OS, type: 'multiple', category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USER_BROWSER]: { key: TYPES.USER_BROWSER, type: 'multiple', category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USER_DEVICE]: { key: TYPES.USER_DEVICE, type: 'multiple', category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.PLATFORM]: { key: TYPES.PLATFORM, type: 'multiple', category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.REVID]: { key: TYPES.REVID, type: 'multiple', category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
|
||||
[TYPES.REFERRER]: { category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.DURATION]: { category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USER_COUNTRY]: { category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.REFERRER]: { key: TYPES.REFERRER, type: 'multiple', category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.DURATION]: { key: TYPES.DURATION, type: 'number', category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USER_COUNTRY]: { key: TYPES.USER_COUNTRY, type: 'multiple', category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
|
||||
[TYPES.CONSOLE]: { category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.ERROR]: { category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.FETCH]: { category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.GRAPHQL]: { category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.STATEACTION]: { category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.CONSOLE]: { key: TYPES.CONSOLE, type: 'multiple', category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.ERROR]: { key: TYPES.ERROR, type: 'multiple', category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.FETCH]: { key: TYPES.FETCH, type: 'multiple', category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.GRAPHQL]: { key: TYPES.GRAPHQL, type: 'multiple', category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.STATEACTION]: { key: TYPES.STATEACTION, type: 'multiple', category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
|
||||
[TYPES.USERID]: { category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USERANONYMOUSID]: { category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USERID]: { key: TYPES.USERID, type: 'multiple', category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.USERANONYMOUSID]: { key: TYPES.USERANONYMOUSID, type: 'multiple', category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
|
||||
[TYPES.DOM_COMPLETE]: { category: 'new', label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.LARGEST_CONTENTFUL_PAINT_TIME]: { category: 'new', label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.TIME_BETWEEN_EVENTS]: { category: 'new', label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.TTFB]: { category: 'new', label: 'TTFB', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.AVG_CPU_LOAD]: { category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.AVG_MEMORY_USAGE]: { category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.SLOW_SESSION]: { category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' },
|
||||
[TYPES.MISSING_RESOURCE]: { category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' },
|
||||
[TYPES.CLICK_RAGE]: { category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' },
|
||||
// [TYPES.URL]: { category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions },
|
||||
// [TYPES.CUSTOM]: { category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions },
|
||||
// [TYPES.METADATA]: { category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions },
|
||||
[TYPES.DOM_COMPLETE]: { key: TYPES.DOM_COMPLETE, type: 'multiple', category: 'new', label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.LARGEST_CONTENTFUL_PAINT_TIME]: { key: TYPES.LARGEST_CONTENTFUL_PAINT_TIME, type: 'number', category: 'new', label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.TIME_BETWEEN_EVENTS]: { key: TYPES.TIME_BETWEEN_EVENTS, type: 'number', category: 'new', label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.TTFB]: { key: TYPES.TTFB, type: 'time', category: 'new', label: 'TTFB', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.AVG_CPU_LOAD]: { key: TYPES.AVG_CPU_LOAD, type: 'number', category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.AVG_MEMORY_USAGE]: { key: TYPES.AVG_MEMORY_USAGE, type: 'number', category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
|
||||
[TYPES.SLOW_SESSION]: { key: TYPES.SLOW_SESSION, type: 'boolean', category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' },
|
||||
[TYPES.MISSING_RESOURCE]: { key: TYPES.MISSING_RESOURCE, type: 'boolean', category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' },
|
||||
[TYPES.CLICK_RAGE]: { key: TYPES.CLICK_RAGE, type: 'boolean', category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' },
|
||||
// [TYPES.URL]: { / [TYPES,TYPES. category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions },
|
||||
// [TYPES.CUSTOM]: { / [TYPES,TYPES. category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions },
|
||||
// [TYPES.METADATA]: { / [TYPES,TYPES. category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions },
|
||||
}
|
||||
|
||||
export default Record({
|
||||
|
|
@ -263,6 +263,7 @@ export default Record({
|
|||
icon: '',
|
||||
type: '',
|
||||
value: [""],
|
||||
category: '',
|
||||
|
||||
custom: '',
|
||||
// target: Target(),
|
||||
|
|
@ -274,10 +275,13 @@ export default Record({
|
|||
|
||||
operator: 'is',
|
||||
operatorOptions: [],
|
||||
isEvent: false,
|
||||
index: 0,
|
||||
}, {
|
||||
keyKey: "_key",
|
||||
fromJS: ({ ...filter }) => ({
|
||||
...filter,
|
||||
key: filter.type,
|
||||
type: filter.type, // camelCased(filter.type.toLowerCase()),
|
||||
// key: filter.type === METADATA ? filter.label : filter.key || filter.type, // || camelCased(filter.type.toLowerCase()),
|
||||
// label: getLabel(filter),
|
||||
|
|
@ -299,10 +303,4 @@ export default Record({
|
|||
// operators: filterMap[key].operatorOptions,
|
||||
// value: [""]
|
||||
// }
|
||||
// }
|
||||
|
||||
// export const newFiltersList = [
|
||||
// NewFilterType(TYPES.CLICK, 'Click', 'filters/click', true),
|
||||
// NewFilterType(TYPES.CLICK, 'Input', 'filters/click', true),
|
||||
// NewFilterType(TYPES.CONSOLE, 'Console', 'filters/click', true),
|
||||
// ];
|
||||
// }
|
||||
Loading…
Add table
Reference in a new issue