feat(ui) - custom metrics

This commit is contained in:
Shekar Siri 2022-01-23 23:25:28 +05:30
parent e80293efa4
commit af260a7530
35 changed files with 963 additions and 239 deletions

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import { Button, Dropdown, Form, Input, SegmentSelection, Checkbox, Message, Link, Icon } from 'UI';
import { alertMetrics as metrics } from 'App/constants';
import { alertConditions as conditions } from 'App/constants';
@ -8,6 +8,7 @@ import stl from './alertForm.css';
import DropdownChips from './DropdownChips';
import { validateEmail } from 'App/validate';
import cn from 'classnames';
import { fetchTriggerOptions } from 'Duck/alerts';
const thresholdOptions = [
{ text: '15 minutes', value: 15 },
@ -46,11 +47,15 @@ const Section = ({ index, title, description, content }) => (
const integrationsRoute = client(CLIENT_TABS.INTEGRATIONS);
const AlertForm = props => {
const { instance, slackChannels, webhooks, loading, onDelete, deleting } = props;
const { instance, slackChannels, webhooks, loading, onDelete, deleting, triggerOptions } = props;
const write = ({ target: { value, name } }) => props.edit({ [ name ]: value })
const writeOption = (e, { name, value }) => props.edit({ [ name ]: value });
const onChangeOption = (e, { checked, name }) => props.edit({ [ name ]: checked })
useEffect(() => {
props.fetchTriggerOptions();
}, [])
const writeQueryOption = (e, { name, value }) => {
const { query } = instance;
props.edit({ query: { ...query, [name] : value } });
@ -61,10 +66,12 @@ const AlertForm = props => {
props.edit({ query: { ...query, [name] : value } });
}
const metric = (instance && instance.query.left) ? metrics.find(i => i.value === instance.query.left) : null;
const metric = (instance && instance.query.left) ? triggerOptions.find(i => i.value === instance.query.left) : null;
const unit = metric ? metric.unit : '';
const isThreshold = instance.detectionMethod === 'threshold';
console.log('triggerOptions', triggerOptions)
return (
<Form className={ cn("p-6", stl.wrapper)} style={{ width: '580px' }} onSubmit={() => props.onSubmit(instance)} id="alert-form">
@ -135,7 +142,7 @@ const AlertForm = props => {
placeholder="Select Metric"
selection
search
options={ metrics }
options={ triggerOptions }
name="left"
value={ instance.query.left }
onChange={ writeQueryOption }
@ -327,6 +334,7 @@ const AlertForm = props => {
export default connect(state => ({
instance: state.getIn(['alerts', 'instance']),
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
deleting: state.getIn(['alerts', 'removeRequest', 'loading'])
}))(AlertForm)
}), { fetchTriggerOptions })(AlertForm)

View file

@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react'
import { SlideModal, IconButton } from 'UI';
import { init, edit, save, remove } from 'Duck/alerts';
import { fetchList as fetchWebhooks } from 'Duck/webhook';
import AlertForm from '../AlertForm';
import { connect } from 'react-redux';
import { setShowAlerts } from 'Duck/dashboard';
import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule';
import { confirm } from 'UI/Confirmation';
interface Props {
showModal?: boolean;
metricId?: number;
onClose: () => void;
}
function AlertFormModal(props) {
const { metricId = null, showModal = false, webhooks, setShowAlerts } = props;
const [showForm, setShowForm] = useState(false);
useEffect(() => {
props.fetchWebhooks();
}, [])
const slackChannels = webhooks.filter(hook => hook.type === SLACK).map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS();
const hooks = webhooks.filter(hook => hook.type === WEBHOOK).map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS();
const saveAlert = instance => {
const wasUpdating = instance.exists();
props.save(instance).then(() => {
if (!wasUpdating) {
toggleForm(null, false);
}
})
}
const onDelete = async (instance) => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, Delete',
confirmation: `Are you sure you want to permanently delete this alert?`
})) {
props.remove(instance.alertId).then(() => {
toggleForm(null, false);
});
}
}
const toggleForm = (instance, state) => {
if (instance) {
props.init(instance)
}
return setShowForm(state ? state : !showForm);
}
return (
<SlideModal
title={
<div className="flex items-center">
<span className="mr-3">{ 'Create Alert' }</span>
<IconButton
circle
size="small"
icon="plus"
outline
id="add-button"
onClick={ () => toggleForm({}, true) }
/>
</div>
}
isDisplayed={ showModal }
onClose={props.onClose}
size="medium"
content={ showModal &&
<AlertForm
metricId={ props.metricId }
edit={props.edit}
slackChannels={slackChannels}
webhooks={hooks}
onSubmit={saveAlert}
onClose={props.onClose}
onDelete={onDelete}
/>
}
/>
);
}
export default connect(state => ({
webhooks: state.getIn(['webhooks', 'list']),
instance: state.getIn(['alerts', 'instance']),
}), { init, edit, save, remove, fetchWebhooks, setShowAlerts })(AlertFormModal)

View file

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

View file

@ -205,7 +205,7 @@ export default class Dashboard extends React.PureComponent {
<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 />
<CustomMetricsWidgets onClickEdit={(e) => null}/>
</div>
</WidgetSection>

View file

@ -0,0 +1,6 @@
.wrapper {
background-color: white;
/* border: solid thin $gray-medium; */
border-radius: 3px;
padding: 10px;
}

View file

@ -1,9 +1,16 @@
import React from 'react';
import { Loader, NoContent } from 'UI';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Loader, NoContent, Icon } 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';
import stl from './CustomMetricWidget.css';
import { getChartFormatter } from 'Types/dashboard/helper';
import { remove, setAlertMetricId } from 'Duck/customMetrics';
import { confirm } from 'UI/Confirmation';
import APIClient from 'App/api_client';
import { setShowAlerts } from 'Duck/dashboard';
const customParams = rangeName => {
const params = { density: 70 }
@ -21,55 +28,117 @@ interface Period {
}
interface Props {
widget: any;
loading?: boolean;
metric: any;
// loading?: boolean;
data?: any;
showSync?: boolean;
compare?: boolean;
period?: Period;
onClickEdit: (e) => void;
remove: (id) => void;
setShowAlerts: (showAlerts) => void;
setAlertMetricId: (id) => void;
onAlertClick: (e) => void;
}
function CustomMetricWidget(props: Props) {
const { widget, loading = false, data = { chart: []}, showSync, compare, period = { rangeName: ''} } = props;
const { metric, showSync, compare, period = { rangeName: LAST_24_HOURS} } = props;
const [loading, setLoading] = useState(false)
const [data, setData] = useState<any>({ chart: [] })
const colors = compare ? Styles.compareColors : Styles.colors;
const params = customParams(period.rangeName)
const gradientDef = Styles.gradientDef();
const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' }
useEffect(() => {
// dataWrapper: (p, period) => SessionsImpactedBySlowRequests({ chart: p})
// .update("chart", getChartFormatter(period))
new APIClient()['post']('/custom_metrics/chart', { ...metricParams, q: metric.name })
.then(response => response.json())
.then(({ errors, data }) => {
if (errors) {
console.log('err', errors)
} else {
// console.log('data', data);
// const _data = data[0].map(CustomMetric).update("chart", getChartFormatter(period)).toJS();
const _data = getChartFormatter(period)(data[0]);
console.log('__data', _data)
setData({ chart: _data });
}
}).finally(() => setLoading(false));
}, [])
const deleteHandler = async () => {
if (await confirm({
header: 'Custom Metric',
confirmButton: 'Delete',
confirmation: `Are you sure you want to delete ${metric.name}`
})) {
props.remove(metric.metricId)
}
}
// const onAlertClick = () => {
// props.setShowAlerts(true)
// props.setAlertMetricId(metric.metricId)
// }
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 }
<div className={stl.wrapper}>
<div className="flex items-center mb-10 p-2">
<div className="font-medium">{metric.name + ' ' + metric.metricId}</div>
<div className="ml-auto flex items-center">
<div className="cursor-pointer mr-6" onClick={deleteHandler}>
<Icon name="trash" size="14" />
</div>
<div className="cursor-pointer mr-6">
<Icon name="pencil" size="14" />
</div>
<div className="cursor-pointer" onClick={props.onAlertClick}>
<Icon name="bell-plus" size="14" />
</div>
</div>
</div>
<div>
<Loader loading={ loading } size="small">
<NoContent
size="small"
show={ data.chart.length === 0 }
>
{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>
<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>
</div>
</div>
);
}
export default CustomMetricWidgetHoc(CustomMetricWidget);
export default connect(null, { remove, setShowAlerts, setAlertMetricId })(CustomMetricWidget);

View file

@ -0,0 +1,6 @@
.wrapper {
background-color: white;
/* border: solid thin $gray-medium; */
border-radius: 3px;
padding: 10px;
}

View file

@ -0,0 +1,110 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Loader, NoContent, Icon } 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 stl from './CustomMetricWidgetPreview.css';
import { getChartFormatter } from 'Types/dashboard/helper';
import { remove } from 'Duck/customMetrics';
import { confirm } from 'UI/Confirmation';
import APIClient from 'App/api_client';
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 {
metric: any;
// loading?: boolean;
data?: any;
showSync?: boolean;
compare?: boolean;
period?: Period;
onClickEdit?: (e) => void;
remove: (id) => void;
}
function CustomMetricWidget(props: Props) {
const { metric, showSync, compare, period = { rangeName: LAST_24_HOURS} } = props;
const [loading, setLoading] = useState(false)
const [data, setData] = useState<any>({ chart: [{}] })
const colors = compare ? Styles.compareColors : Styles.colors;
const params = customParams(period.rangeName)
const gradientDef = Styles.gradientDef();
const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' }
useEffect(() => {
new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
.then(response => response.json())
.then(({ errors, data }) => {
if (errors) {
console.log('err', errors)
} else {
const _data = getChartFormatter(period)(data[0]);
console.log('__data', _data)
setData({ chart: _data });
}
}).finally(() => setLoading(false));
}, [metric])
return (
<div className={stl.wrapper}>
<div className="flex items-center mb-10 p-2">
</div>
<div>
<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>
</div>
</div>
);
}
export default connect(null, { remove })(CustomMetricWidget);

View file

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

View file

@ -1,15 +1,18 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } 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';
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
interface Props {
fetchList: Function;
list: any;
onClickEdit: (e) => void;
}
function CustomMetricsWidgets(props: Props) {
const { list } = props;
const [activeMetricId, setActiveMetricId] = useState(null);
useEffect(() => {
props.fetchList()
@ -18,8 +21,18 @@ function CustomMetricsWidgets(props: Props) {
return (
<>
{list.map((item: any) => (
<CustomMetricWidget widget={item} />
<CustomMetricWidget
metric={item}
onClickEdit={props.onClickEdit}
onAlertClick={(e) => setActiveMetricId(item.metricId)}
/>
))}
<AlertFormModal
showModal={!!activeMetricId}
metricId={activeMetricId}
onClose={() => setActiveMetricId(null)}
/>
</>
);
}

View file

@ -3,6 +3,7 @@ import { Form, SegmentSelection, Button, IconButton } from 'UI';
import FilterSeries from '../FilterSeries';
import { connect } from 'react-redux';
import { edit as editMetric, save } from 'Duck/customMetrics';
import CustomMetricWidgetPreview from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview';
interface Props {
metric: any;
@ -50,7 +51,7 @@ function CustomMetricForm(props: Props) {
className="relative"
onSubmit={() => props.save(metric)}
>
<div className="p-5" style={{ height: 'calc(100vh - 60px)', overflowY: 'auto' }}>
<div className="p-5 pb-20" style={{ height: 'calc(100vh - 60px)', overflowY: 'auto' }}>
<div className="form-group">
<label className="font-medium">Metric Title</label>
<input
@ -103,9 +104,13 @@ function CustomMetricForm(props: Props) {
<div className="flex justify-end">
<IconButton onClick={addSeries} primaryText label="SERIES" icon="plus" />
</div>
<div className="my-4" />
<CustomMetricWidgetPreview metric={metric} />
</div>
<div className="absolute w-full bottom-0 px-5 py-2 bg-white">
<div className="fixed border-t w-full bottom-0 px-5 py-2 bg-white">
<Button loading={loading} primary>
Save
</Button>

View file

@ -1,14 +1,28 @@
import CustomMetricWidgetPreview from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview';
import React, { useState } from 'react';
import { IconButton, SlideModal } from 'UI'
import CustomMetricForm from './CustomMetricForm';
import { connect } from 'react-redux';
import { edit } from 'Duck/customMetrics';
interface Props {}
interface Props {
metric: any;
edit: (metric) => void;
}
function CustomMetrics(props: Props) {
const [showModal, setShowModal] = useState(true);
const { metric } = props;
const [showModal, setShowModal] = useState(false);
const onClose = () => {
setShowModal(false);
}
return (
<div className="self-start">
<IconButton outline icon="plus" label="CREATE METRIC" onClick={() => setShowModal(true)} />
<IconButton outline icon="plus" label="CREATE METRIC" onClick={() => {
setShowModal(true);
// props.edit({ name: 'New', series: [{ name: '', filter: {} }], type: '' });
}} />
<SlideModal
title={
@ -19,9 +33,9 @@ function CustomMetrics(props: Props) {
isDisplayed={ showModal }
onClose={ () => setShowModal(false)}
// size="medium"
content={ showModal && (
<div style={{ backgroundColor: '#f6f6f6'}}>
<CustomMetricForm />
content={ (showModal || metric) && (
<div style={{ backgroundColor: '#f6f6f6' }}>
<CustomMetricForm metric={metric} />
</div>
)}
/>
@ -29,4 +43,7 @@ function CustomMetrics(props: Props) {
);
}
export default CustomMetrics;
export default connect(state => ({
metric: state.getIn(['customMetrics', 'instance']),
alertInstance: state.getIn(['alerts', 'instance']),
}), { edit })(CustomMetrics);

View file

@ -4,6 +4,7 @@ import { edit, updateSeries } from 'Duck/customMetrics';
import { connect } from 'react-redux';
import { IconButton, Button, Icon, SegmentSelection } from 'UI';
import FilterSelection from '../../Filters/FilterSelection';
import SeriesName from './SeriesName';
interface Props {
seriesIndex: number;
@ -14,7 +15,7 @@ interface Props {
}
function FilterSeries(props: Props) {
const [expanded, setExpanded] = useState(false)
const [expanded, setExpanded] = useState(true)
const { series, seriesIndex } = props;
const onAddFilter = (filter) => {
@ -74,9 +75,15 @@ function FilterSeries(props: Props) {
return (
<div className="border rounded bg-white">
<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 className="font-medium flex items-center">
{ series.name }
<div className="ml-3 cursor-pointer"><Icon name="pencil" size="14" /></div>
</div> */}
<div className="mr-auto">
<SeriesName name={series.name} onUpdate={() => null } />
</div>
<div className="flex items-center cursor-pointer" >
<div onClick={props.onRemoveSeries} className="ml-3">
<Icon name="trash" size="16" />
</div>

View file

@ -0,0 +1,46 @@
import { edit } from 'App/components/ui/ItemMenu/itemMenu.css';
import React, { useState, useRef, useEffect } from 'react';
import { Icon } from 'UI';
interface Props {
name: string;
onUpdate: (name) => void;
}
function SeriesName(props: Props) {
const [editing, setEditing] = useState(false)
const [name, setName] = useState(props.name)
const ref = useRef<any>(null)
const write = ({ target: { value, name } }) => {
setName(value)
}
const onBlur = () => {
setEditing(false)
// props.onUpdate(name)
}
useEffect(() => {
if (editing) {
ref.current.focus()
}
}, [editing])
// const { name } = props;
return (
<div className="font-medium flex items-center">
<input
ref={ ref }
name="name"
className="fluid border-0 -mx-2 px-2"
value={name} readOnly={!editing}
onChange={write}
onBlur={onBlur}
onFocus={() => setEditing(true)}
/>
<div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div>
</div>
);
}
export default SeriesName;

View file

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

View file

@ -101,7 +101,7 @@ function FilterAutoComplete(props: Props) {
<input
name="query"
onChange={ onInputChange }
onBlur={ () => setTimeout(() => { setShowModal(false) }, 10) }
onBlur={ () => setTimeout(() => { setShowModal(false) }, 50) }
onFocus={ () => setShowModal(true)}
value={ query }
autoFocus={ true }

View file

@ -0,0 +1,24 @@
.wrapper {
display: flex;
justify-content: space-between;
& input {
max-width: 85px !important;
font-size: 13px !important;
font-weight: 400 !important;
color: $gray-medium !important;
}
& > div {
&:first-child {
margin-right: 10px;
}
}
}
.label {
font-size: 13px !important;
font-weight: 400 !important;
color: $gray-medium !important;
}

View file

@ -0,0 +1,66 @@
import { Input, Label } from 'semantic-ui-react';
import styles from './FilterDuration.css';
const fromMs = value => value ? `${ value / 1000 / 60 }` : ''
const toMs = value => value !== '' ? value * 1000 * 60 : null
export default class FilterDuration extends React.PureComponent {
state = { focused: false }
onChange = (e, { name, value }) => {
const { onChange } = this.props;
if (typeof onChange === 'function') {
onChange({
[ name ]: toMs(value),
});
}
}
onKeyPress = e => {
const { onEnterPress } = this.props;
if (e.key === 'Enter' && typeof onEnterPress === 'function') {
onEnterPress(e);
}
}
render() {
const {
minDuration,
maxDuration,
} = this.props;
return (
<div className={ styles.wrapper }>
<Input
labelPosition="left"
type="number"
placeholder="0 min"
name="minDuration"
value={ fromMs(minDuration) }
onChange={ this.onChange }
className="customInput"
onKeyPress={ this.onKeyPress }
onFocus={() => this.setState({ focused: true })}
onBlur={this.props.onBlur}
>
<Label basic className={ styles.label }>{ 'Min' }</Label>
<input min="1" />
</Input>
<Input
labelPosition="left"
type="number"
placeholder="∞ min"
name="maxDuration"
value={ fromMs(maxDuration) }
onChange={ this.onChange }
className="customInput"
onKeyPress={ this.onKeyPress }
onFocus={() => this.setState({ focused: true })}
onBlur={this.props.onBlur}
>
<Label basic className={ styles.label }>{ 'Max' }</Label>
<input min="1" />
</Input>
</div>
);
}
}

View file

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

View file

@ -17,25 +17,25 @@ function FitlerItem(props: Props) {
onUpdate(filter);
};
const onAddValue = () => {
const newValues = filter.value.concat("")
onUpdate({ ...filter, value: newValues })
}
// const onAddValue = () => {
// const newValues = filter.value.concat("")
// onUpdate({ ...filter, value: newValues })
// }
const onRemoveValue = (valueIndex) => {
const newValues = filter.value.filter((_, _index) => _index !== valueIndex)
onUpdate({ ...filter, value: newValues })
}
// const onRemoveValue = (valueIndex) => {
// const newValues = filter.value.filter((_, _index) => _index !== valueIndex)
// onUpdate({ ...filter, value: newValues })
// }
const onSelect = (e, item, valueIndex) => {
const newValues = filter.value.map((_, _index) => {
if (_index === valueIndex) {
return item.value;
}
return _;
})
onUpdate({ ...filter, value: newValues })
}
// const onSelect = (e, item, valueIndex) => {
// const newValues = filter.value.map((_, _index) => {
// if (_index === valueIndex) {
// return item.value;
// }
// return _;
// })
// onUpdate({ ...filter, value: newValues })
// }
console.log('filter', filter);
@ -49,22 +49,23 @@ function FitlerItem(props: Props) {
<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 flex-shrink-0"/>
<div className="grid grid-cols-3 gap-3">
{filter.value && filter.value.map((value, valueIndex) => (
{/* <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}
// filter={filter}
// showCloseButton={filter.value.length > 1}
// showOrButton={valueIndex === filter.value.length - 1}
filter={filter}
onUpdate={onUpdate}
// key={valueIndex}
value={value}
key={filter.key}
index={valueIndex}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
onSelect={(e, item) => onSelect(e, item, valueIndex)}
// value={value}
// key={filter.key}
// index={valueIndex}
// onAddValue={onAddValue}
// onRemoveValue={(valueIndex) => onRemoveValue(valueIndex)}
// onSelect={(e, item, valueIndex) => onSelect(e, item, valueIndex)}
/>
))}
</div>
{/* ))} */}
{/* </div> */}
</div>
<div className="flex self-start mt-2">
<div

View file

@ -5,14 +5,11 @@ import stl from './FilterOperator.css';
interface Props {
filter: any; // event/filter
// options: any[];
// value: string;
onChange: (e, { name, value }) => void;
className?: string;
}
function FilterOperator(props: Props) {
const { filter, onChange, className = '' } = props;
const options = []
return (
<Dropdown
@ -21,6 +18,7 @@ function FilterOperator(props: Props) {
name="operator"
value={ filter.operator }
onChange={ onChange }
placeholder="Select operator"
icon={ <Icon className="ml-5" name="chevron-down" size="12" /> }
/>
);

View file

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

View file

@ -1,34 +1,92 @@
import React from 'react';
import FilterAutoComplete from '../FilterAutoComplete';
import { FilterType } from 'Types/filter/filterType';
import FilterValueDropdown from '../FilterValueDropdown';
import FilterDuration from '../FilterDuration';
interface Props {
index: number;
value: any; // event/filter
// type: string;
key: string;
onRemoveValue?: () => void;
onAddValue?: () => void;
showCloseButton: boolean;
showOrButton: boolean;
onSelect: (e, item) => void;
filter: any;
onUpdate: (filter) => void;
}
function FilterValue(props: Props) {
const { index, value, key, showOrButton, showCloseButton, onRemoveValue , onAddValue } = props;
const { filter } = props;
const onAddValue = () => {
const newValues = filter.value.concat("")
props.onUpdate({ ...filter, value: newValues })
}
const onRemoveValue = (valueIndex) => {
const newValues = filter.value.filter((_, _index) => _index !== valueIndex)
props.onUpdate({ ...filter, value: newValues })
}
const onSelect = (e, item, valueIndex) => {
const newValues = filter.value.map((_, _index) => {
if (_index === valueIndex) {
return item.value;
}
return _;
})
props.onUpdate({ ...filter, value: newValues })
}
const renderValueFiled = (value, valueIndex) => {
switch(filter.type) {
case FilterType.ISSUE:
return (
<FilterValueDropdown
value={value}
filter={filter}
options={filter.options}
onChange={(e, { name, value }) => onSelect(e, { value }, valueIndex)}
/>
)
case FilterType.DURATION:
return (
<FilterDuration
// onChange={ this.onDurationChange }
// onEnterPress={ this.handleClose }
// onBlur={this.handleClose}
minDuration={ filter.value[0] }
maxDuration={ filter.value[1] }
/>
)
case FilterType.NUMBER:
return (
<input
className="w-full px-2 py-1 text-sm leading-tight text-gray-700 rounded-lg"
type="number"
name={`${filter.key}-${valueIndex}`}
value={value}
onChange={(e) => onSelect(e, { value: e.target.value }, valueIndex)}
/>
)
case FilterType.MULTIPLE:
return (
<FilterAutoComplete
value={value}
showCloseButton={filter.value.length > 1}
showOrButton={valueIndex === filter.value.length - 1}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
method={'GET'}
endpoint='/events/search'
params={{ type: filter.key }}
headerText={''}
// placeholder={''}
onSelect={(e, item) => onSelect(e, item, valueIndex)}
/>
)
}
}
return (
<FilterAutoComplete
value={value}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
onAddValue={onAddValue}
onRemoveValue={onRemoveValue}
method={'GET'}
endpoint='/events/search'
params={{ type: key }}
headerText={''}
// placeholder={''}
onSelect={props.onSelect}
/>
<div className="grid grid-cols-3 gap-3">
{filter.value && filter.value.map((value, valueIndex) => (
renderValueFiled(value, valueIndex)
))}
</div>
);
}

View file

@ -0,0 +1,19 @@
.operatorDropdown {
font-weight: 400;
height: 30px;
min-width: 60px;
display: flex !important;
align-items: center;
justify-content: space-between;
padding: 0 8px !important;
font-size: 13px;
/* background-color: rgba(255, 255, 255, 0.8) !important; */
background-color: $gray-lightest !important;
border: solid thin rgba(34, 36, 38, 0.15) !important;
border-radius: 4px !important;
color: $gray-darkest !important;
font-size: 14px !important;
&.ui.basic.button {
box-shadow: 0 0 0 1px rgba(62, 170, 175,36,38,.35) inset, 0 0 0 0 rgba(62, 170, 175,.15) inset !important;
}
}

View file

@ -0,0 +1,31 @@
import React from 'react';
import cn from 'classnames';
import { Dropdown, Icon } from 'UI';
import stl from './FilterValueDropdown.css';
interface Props {
filter: any; // event/filter
// options: any[];
value: string;
onChange: (e, { name, value }) => void;
className?: string;
options: any[];
}
function FilterValueDropdown(props: Props) {
const { options, onChange, value, className = '' } = props;
// const options = []
return (
<Dropdown
className={ cn(stl.operatorDropdown, className) }
options={ options }
name="issue_type"
value={ value }
onChange={ onChange }
placeholder="Select"
icon={ <Icon className="ml-5" name="chevron-down" size="12" /> }
/>
);
}
export default FilterValueDropdown;

View file

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

View file

@ -10,7 +10,7 @@
color: $gray-medium;
font-weight: medium;
padding: 10px;
/* flex: 1; */
flex: 1;
text-align: center;
border-right: solid thin $teal;
cursor: pointer;
@ -18,6 +18,7 @@
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
& span svg {
fill: $gray-medium;

View file

@ -1,9 +1,38 @@
import Alert from 'Types/alert';
import { Map } from 'immutable';
import crudDuckGenerator from './tools/crudDuck';
import withRequestState, { RequestTypes } from 'Duck/requestStateCreator';
import { reduceDucks } from 'Duck/tools';
const name = 'alert'
const idKey = 'alertId';
const crudDuck = crudDuckGenerator('alert', Alert, { idKey: idKey });
const crudDuck = crudDuckGenerator(name, Alert, { idKey: idKey });
export const { fetchList, init, edit, remove } = crudDuck.actions;
const FETCH_TRIGGER_OPTIONS = new RequestTypes(`${name}/FETCH_TRIGGER_OPTIONS`);
const initialState = Map({
definedPercent: 0,
triggerOptions: [],
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
// case GENERATE_LINK.SUCCESS:
// return state.update(
// 'list',
// list => list
// .map(member => {
// if(member.id === action.id) {
// return Member({...member.toJS(), invitationLink: action.data.invitationLink })
// }
// return member
// })
// );
case FETCH_TRIGGER_OPTIONS.SUCCESS:
return state.set('triggerOptions', action.data);
}
return state;
};
export function save(instance) {
return {
@ -12,4 +41,12 @@ export function save(instance) {
};
}
export default crudDuck.reducer;
export function fetchTriggerOptions() {
return {
types: FETCH_TRIGGER_OPTIONS.toArray(),
call: client => client.get('/alerts/triggers'),
};
}
// export default crudDuck.reducer;
export default reduceDucks(crudDuck, { initialState, reducer }).reducer;

View file

@ -2,7 +2,7 @@ import { List, Map } from 'immutable';
import { clean as cleanParams } from 'App/api_client';
import ErrorInfo, { RESOLVED, UNRESOLVED, IGNORED } from 'Types/errorInfo';
import CustomMetric, { FilterSeries } from 'Types/customMetric'
import { createFetch, fetchListType, fetchType, saveType, editType, createEdit } from './funcTools/crud';
import { createFetch, fetchListType, fetchType, saveType, removeType, editType, createRemove, createEdit } from './funcTools/crud';
// import { createEdit, createInit } from './funcTools/crud';
import { createRequestReducer, ROOT_KEY } from './funcTools/request';
import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools';
@ -18,7 +18,9 @@ const FETCH_LIST = fetchListType(name);
const FETCH = fetchType(name);
const SAVE = saveType(name);
const EDIT = editType(name);
const REMOVE = removeType(name);
const UPDATE_SERIES = `${name}/UPDATE_SERIES`;
const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`;
function chartWrapper(chart = []) {
return chart.map(point => ({ ...point, count: Math.max(point.count, 0) }));
@ -31,6 +33,8 @@ function chartWrapper(chart = []) {
const initialState = Map({
list: List(),
alertMetricId: null,
// instance: null,
instance: CustomMetric({
name: 'New',
series: List([
@ -46,14 +50,18 @@ const initialState = Map({
function reducer(state = initialState, action = {}) {
switch (action.type) {
case EDIT:
console.log('EDIT', action);
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));
case success(REMOVE):
console.log('action', action)
return state.update('list', list => list.filter(item => item.metricId !== action.id));
case success(FETCH):
return state.set("instance", ErrorInfo(action.data));
return state.set("instance", ErrorInfo(action.data));
case success(FETCH_LIST):
const { data } = action;
return state.set("list", List(data.map(CustomMetric)));
@ -70,6 +78,7 @@ export default mergeReducers(
);
export const edit = createEdit(name);
export const remove = createRemove(name);
export const updateSeries = (index, series) => ({
type: UPDATE_SERIES,
@ -88,7 +97,7 @@ export function fetch(id) {
export function save(instance) {
return {
types: SAVE.array,
call: client => client.post( `/${ name }s`, instance.toData()),
call: client => client.post( `/${ name }s`, instance.toSaveData()),
};
}
@ -97,4 +106,11 @@ export function fetchList() {
types: array(FETCH_LIST),
call: client => client.get(`/${name}s`),
};
}
export function setAlertMetricId(id) {
return {
type: SET_ALERT_METRIC_ID,
id,
};
}

View file

@ -17,8 +17,6 @@ export const FilterSeries = Record({
methods: {
toData() {
const js = this.toJS();
delete js.key;
// js.filter = js.filter.toData();
return js;
},
},
@ -41,15 +39,19 @@ export default Record({
return validateName(this.name, { diacritics: true });
},
toData() {
toSaveData() {
const js = this.toJS();
js.series = js.series.map(series => {
series.filter.filters = series.filter.filters.map(filter => {
filter.type = filter.key
delete filter.operatorOptions
delete filter.icon
delete filter.key
delete filter._key
return filter;
});
delete series._key
return series;
});
@ -57,6 +59,11 @@ export default Record({
return js;
},
toData() {
const js = this.toJS();
return js;
},
},
fromJS: ({ series, ...rest }) => ({
...rest,

View file

@ -0,0 +1,14 @@
import { Record } from 'immutable';
const CustomMetric = Record({
avg: undefined,
chart: [],
});
function fromJS(data = {}) {
if (data instanceof CustomMetric) return data;
return new CustomMetric(data);
}
export default fromJS;

View file

@ -53,8 +53,8 @@ export default Record({
toData() {
const js = this.toJS();
js.filters = js.filters.map(filter => {
delete filter.operatorOptions
delete filter._key
// delete filter.operatorOptions
// delete filter._key
return filter;
});

View file

@ -0,0 +1,49 @@
export enum FilterType {
ISSUE = "ISSUE",
BOOLEAN = "BOOLEAN",
NUMBER = "NUMBER",
DURATION = "DURATION",
MULTIPLE = "MULTIPLE",
COUNTRY = "COUNTRY",
};
export enum FilterKey {
ERROR = "ERROR",
MISSING_RESOURCE = "MISSING_RESOURCE",
SLOW_SESSION = "SLOW_SESSION",
CLICK_RAGE = "CLICK_RAGE",
CLICK = "CLICK",
INPUT = "INPUT",
LOCATION = "LOCATION",
VIEW = "VIEW",
CONSOLE = "CONSOLE",
METADATA = "METADATA",
CUSTOM = "CUSTOM",
URL = "URL",
USER_BROWSER = "USERBROWSER",
USER_OS = "USEROS",
USER_DEVICE = "USERDEVICE",
PLATFORM = "PLATFORM",
DURATION = "DURATION",
REFERRER = "REFERRER",
USER_COUNTRY = "USER_COUNTRY",
JOURNEY = "JOURNEY",
FETCH = "FETCH",
GRAPHQL = "GRAPHQL",
STATEACTION = "STATEACTION",
REVID = "REVID",
USERANONYMOUSID = "USERANONYMOUSID",
USERID = "USERID",
ISSUE = "ISSUE",
EVENTS_COUNT = "EVENTS_COUNT",
UTM_SOURCE = "UTM_SOURCE",
UTM_MEDIUM = "UTM_MEDIUM",
UTM_CAMPAIGN = "UTM_CAMPAIGN",
DOM_COMPLETE = "DOM_COMPLETE",
LARGEST_CONTENTFUL_PAINT_TIME = "LARGEST_CONTENTFUL_PAINT_TIME",
TIME_BETWEEN_EVENTS = "TIME_BETWEEN_EVENTS",
TTFB = "TTFB",
AVG_CPU_LOAD = "AVG_CPU_LOAD",
AVG_MEMORY_USAGE = "AVG_MEMORY_USAGE",
}

View file

@ -1,86 +1,92 @@
import Record from 'Types/Record';
import { FilterType, FilterKey } from './filterType'
const CLICK = 'CLICK';
const INPUT = 'INPUT';
const LOCATION = 'LOCATION';
const VIEW = 'VIEW_IOS';
const CONSOLE = 'ERROR';
const METADATA = 'METADATA';
const CUSTOM = 'CUSTOM';
const URL = 'URL';
const CLICK_RAGE = 'CLICKRAGE';
const USER_BROWSER = 'USERBROWSER';
const USER_OS = 'USEROS';
const USER_COUNTRY = 'USERCOUNTRY';
const USER_DEVICE = 'USERDEVICE';
const PLATFORM = 'PLATFORM';
const DURATION = 'DURATION';
const REFERRER = 'REFERRER';
const ERROR = 'ERROR';
const MISSING_RESOURCE = 'MISSINGRESOURCE';
const SLOW_SESSION = 'SLOWSESSION';
const JOURNEY = 'JOUNRNEY';
const FETCH = 'REQUEST';
const GRAPHQL = 'GRAPHQL';
const STATEACTION = 'STATEACTION';
const REVID = 'REVID';
const USERANONYMOUSID = 'USERANONYMOUSID';
const USERID = 'USERID';
export const CLICK = 'CLICK';
export const INPUT = 'INPUT';
export const LOCATION = 'LOCATION';
export const VIEW = 'VIEW_IOS';
export const CONSOLE = 'ERROR';
export const METADATA = 'METADATA';
export const CUSTOM = 'CUSTOM';
export const URL = 'URL';
export const CLICK_RAGE = 'CLICKRAGE';
export const USER_BROWSER = 'USERBROWSER';
export const USER_OS = 'USEROS';
export const USER_COUNTRY = 'USERCOUNTRY';
export const USER_DEVICE = 'USERDEVICE';
export const PLATFORM = 'PLATFORM';
export const DURATION = 'DURATION';
export const REFERRER = 'REFERRER';
export const ERROR = 'ERROR';
export const MISSING_RESOURCE = 'MISSINGRESOURCE';
export const SLOW_SESSION = 'SLOWSESSION';
export const JOURNEY = 'JOUNRNEY';
export const FETCH = 'REQUEST';
export const GRAPHQL = 'GRAPHQL';
export const STATEACTION = 'STATEACTION';
export const REVID = 'REVID';
export const USERANONYMOUSID = 'USERANONYMOUSID';
export const USERID = 'USERID';
const ISSUE = 'ISSUE';
const EVENTS_COUNT = 'EVENTS_COUNT';
const UTM_SOURCE = 'UTM_SOURCE';
const UTM_MEDIUM = 'UTM_MEDIUM';
const UTM_CAMPAIGN = 'UTM_CAMPAIGN';
export const ISSUE = 'ISSUE';
export const EVENTS_COUNT = 'EVENTS_COUNT';
export const UTM_SOURCE = 'UTM_SOURCE';
export const UTM_MEDIUM = 'UTM_MEDIUM';
export const UTM_CAMPAIGN = 'UTM_CAMPAIGN';
const DOM_COMPLETE = 'DOM_COMPLETE';
const LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME';
const TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS';
const TTFB = 'TTFB';
const AVG_CPU_LOAD = 'AVG_CPU_LOAD';
const AVG_MEMORY_USAGE = 'AVG_MEMORY_USAGE';
export const DOM_COMPLETE = 'DOM_COMPLETE';
export const LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME';
export const TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS';
export const TTFB = 'TTFB';
export const AVG_CPU_LOAD = 'AVG_CPU_LOAD';
export const AVG_MEMORY_USAGE = 'AVG_MEMORY_USAGE';
export const TYPES = {
ERROR,
MISSING_RESOURCE,
SLOW_SESSION,
CLICK_RAGE,
CLICK,
INPUT,
LOCATION,
VIEW,
CONSOLE,
METADATA,
CUSTOM,
URL,
USER_BROWSER,
USER_OS,
USER_DEVICE,
PLATFORM,
DURATION,
REFERRER,
USER_COUNTRY,
JOURNEY,
FETCH,
GRAPHQL,
STATEACTION,
REVID,
USERANONYMOUSID,
USERID,
ISSUE,
EVENTS_COUNT,
UTM_SOURCE,
UTM_MEDIUM,
UTM_CAMPAIGN,
const ISSUE_OPTIONS = [
{ text: 'Click Range', value: 'click_rage' },
{ text: 'Dead Click', value: 'dead_click' },
]
// export const TYPES = {
// ERROR,
// MISSING_RESOURCE,
// SLOW_SESSION,
// CLICK_RAGE,
// CLICK,
// INPUT,
// LOCATION,
// VIEW,
// CONSOLE,
// METADATA,
// CUSTOM,
// URL,
// USER_BROWSER,
// USER_OS,
// USER_DEVICE,
// PLATFORM,
// DURATION,
// REFERRER,
// USER_COUNTRY,
// JOURNEY,
// FETCH,
// GRAPHQL,
// STATEACTION,
// REVID,
// USERANONYMOUSID,
// USERID,
// ISSUE,
// EVENTS_COUNT,
// UTM_SOURCE,
// UTM_MEDIUM,
// UTM_CAMPAIGN,
DOM_COMPLETE,
LARGEST_CONTENTFUL_PAINT_TIME,
TIME_BETWEEN_EVENTS,
TTFB,
AVG_CPU_LOAD,
AVG_MEMORY_USAGE,
};
// DOM_COMPLETE,
// LARGEST_CONTENTFUL_PAINT_TIME,
// TIME_BETWEEN_EVENTS,
// TTFB,
// AVG_CPU_LOAD,
// AVG_MEMORY_USAGE,
// };
const filterKeys = ['is', 'isNot'];
const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith'];
@ -219,41 +225,43 @@ export const booleanOptions = [
]
export const filtersMap = {
[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 },
[FilterKey.CLICK]: { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: 'interactions', label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click', isEvent: true },
[FilterKey.INPUT]: { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true },
[FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true },
[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' },
[FilterKey.USER_OS]: { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE, category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: 'gear', label: 'RevId', 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' },
[FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.NUMBER, category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE, category: 'recording_attributes', label: 'User Country', 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' },
[FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.ERROR]: { key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.MULTIPLE, category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.GRAPHQL]: { key: FilterKey.GRAPHQL, type: FilterType.MULTIPLE, category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.STATEACTION]: { key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: 'javascript', label: 'StateAction', 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' },
[FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[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 },
[FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: 'new', label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.NUMBER, category: 'new', label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.TIME_BETWEEN_EVENTS]: { key: FilterKey.TIME_BETWEEN_EVENTS, type: FilterType.NUMBER, category: 'new', label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.TTFB]: { key: FilterKey.TTFB, type: 'time', category: 'new', label: 'TTFB', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.NUMBER, category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.NUMBER, category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
// [FilterKey.SLOW_SESSION]: { key: FilterKey.SLOW_SESSION, type: FilterType.BOOLEAN, category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' },
[FilterKey.MISSING_RESOURCE]: { key: FilterKey.MISSING_RESOURCE, type: FilterType.BOOLEAN, category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' },
// [FilterKey.CLICK_RAGE]: { key: FilterKey.CLICK_RAGE, type: FilterType.BOOLEAN, category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' },
[FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: 'new', label: 'Issue', operator: 'onAnything', operatorOptions: filterOptions, icon: 'filters/click', options: ISSUE_OPTIONS },
// [FilterKey.URL]: { / [TYPES,TYPES. category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions },
// [FilterKey.CUSTOM]: { / [TYPES,TYPES. category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions },
// [FilterKey.METADATA]: { / [TYPES,TYPES. category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions },
}
export default Record({
@ -277,16 +285,17 @@ export default Record({
operatorOptions: [],
isEvent: false,
index: 0,
options: [],
}, {
keyKey: "_key",
fromJS: ({ ...filter }) => ({
fromJS: ({ key, ...filter }) => ({
...filter,
key: filter.type,
key,
type: filter.type, // camelCased(filter.type.toLowerCase()),
// key: filter.type === METADATA ? filter.label : filter.key || filter.type, // || camelCased(filter.type.toLowerCase()),
// label: getLabel(filter),
// target: Target(target),
// operator: getOperatorDefault(filter.type),
operator: getOperatorDefault(key),
// value: target ? target.label : filter.value,
// value: typeof value === 'string' ? [value] : value,
// icon: filter.type ? getfilterIcon(filter.type) : 'filters/metadata'
@ -303,4 +312,14 @@ export default Record({
// operators: filterMap[key].operatorOptions,
// value: [""]
// }
// }
// }
const getOperatorDefault = (type) => {
if (type === MISSING_RESOURCE) return 'true';
if (type === SLOW_SESSION) return 'true';
if (type === CLICK_RAGE) return 'true';
if (type === CLICK) return 'on';
return 'is';
}

View file

@ -11,7 +11,7 @@ const oss = {
CAPTCHA_ENABLED: process.env.CAPTCHA_ENABLED === 'true',
CAPTCHA_SITE_KEY: process.env.CAPTCHA_SITE_KEY,
ORIGIN: () => 'window.location.origin',
API_EDP: "https://dol.openreplay.com/api",
API_EDP: "https://foss.openreplay.com/api",
ASSETS_HOST: () => 'window.location.origin + "/assets"',
VERSION: '1.3.6',
SOURCEMAP: true,