feat(ui) - custom metrics
This commit is contained in:
parent
e80293efa4
commit
af260a7530
35 changed files with 963 additions and 239 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
1
frontend/app/components/Alerts/AlertFormModal/index.ts
Normal file
1
frontend/app/components/Alerts/AlertFormModal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AlertFormModal';
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
.wrapper {
|
||||
background-color: white;
|
||||
/* border: solid thin $gray-medium; */
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.wrapper {
|
||||
background-color: white;
|
||||
/* border: solid thin $gray-medium; */
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricWidgetPreview';
|
||||
|
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SeriesName';
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterDuration';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" /> }
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterOperator';
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterValueDropdown';
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
14
frontend/app/types/dashboard/customMetric.js
Normal file
14
frontend/app/types/dashboard/customMetric.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
49
frontend/app/types/filter/filterType.ts
Normal file
49
frontend/app/types/filter/filterType.ts
Normal 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",
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue