diff --git a/frontend/app/components/Alerts/AlertForm.js b/frontend/app/components/Alerts/AlertForm.js index deb0fa405..692191c96 100644 --- a/frontend/app/components/Alerts/AlertForm.js +++ b/frontend/app/components/Alerts/AlertForm.js @@ -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 (
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) diff --git a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx new file mode 100644 index 000000000..5cf30c278 --- /dev/null +++ b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx @@ -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 ( + + { 'Create Alert' } + toggleForm({}, true) } + /> + + } + isDisplayed={ showModal } + onClose={props.onClose} + size="medium" + content={ showModal && + + } + /> + ); +} + +export default connect(state => ({ + webhooks: state.getIn(['webhooks', 'list']), + instance: state.getIn(['alerts', 'instance']), +}), { init, edit, save, remove, fetchWebhooks, setShowAlerts })(AlertFormModal) \ No newline at end of file diff --git a/frontend/app/components/Alerts/AlertFormModal/index.ts b/frontend/app/components/Alerts/AlertFormModal/index.ts new file mode 100644 index 000000000..6eb4de1f2 --- /dev/null +++ b/frontend/app/components/Alerts/AlertFormModal/index.ts @@ -0,0 +1 @@ +export { default } from './AlertFormModal'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Dashboard.js b/frontend/app/components/Dashboard/Dashboard.js index 81d7a1de9..ecbfc9daa 100644 --- a/frontend/app/components/Dashboard/Dashboard.js +++ b/frontend/app/components/Dashboard/Dashboard.js @@ -205,7 +205,7 @@ export default class Dashboard extends React.PureComponent {
- + null}/>
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.css b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.css new file mode 100644 index 000000000..1d1ef3ee4 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.css @@ -0,0 +1,6 @@ +.wrapper { + background-color: white; + /* border: solid thin $gray-medium; */ + border-radius: 3px; + padding: 10px; +} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.tsx index 9de11f57e..21b04b809 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.tsx @@ -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({ 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 ( - - - - +
+
{metric.name + ' ' + metric.metricId}
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + - {gradientDef} - - - - - - - - - + + + {gradientDef} + + + + + + + + + +
+ ); } -export default CustomMetricWidgetHoc(CustomMetricWidget); \ No newline at end of file +export default connect(null, { remove, setShowAlerts, setAlertMetricId })(CustomMetricWidget); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.css b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.css new file mode 100644 index 000000000..1d1ef3ee4 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.css @@ -0,0 +1,6 @@ +.wrapper { + background-color: white; + /* border: solid thin $gray-medium; */ + border-radius: 3px; + padding: 10px; +} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx new file mode 100644 index 000000000..8c09ed389 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx @@ -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({ 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 ( +
+
+ +
+
+ + + + + {gradientDef} + + + + + + + + + +
+
+ ); +} + +export default connect(null, { remove })(CustomMetricWidget); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/index.ts new file mode 100644 index 000000000..9595513c4 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/index.ts @@ -0,0 +1 @@ +export { default } from './CustomMetricWidgetPreview'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricsWidgets.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricsWidgets.tsx index 3ea473f61..632c7dc22 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricsWidgets.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricsWidgets.tsx @@ -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) => ( - + setActiveMetricId(item.metricId)} + /> ))} + + setActiveMetricId(null)} + /> ); } diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx index 243a51ced..60bcce7a9 100644 --- a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx +++ b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx @@ -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)} > -
+
+ +
+ +
-
+
diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx index c11682513..158ef73c5 100644 --- a/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx +++ b/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx @@ -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 (
- setShowModal(true)} /> + { + setShowModal(true); + // props.edit({ name: 'New', series: [{ name: '', filter: {} }], type: '' }); + }} /> setShowModal(false)} // size="medium" - content={ showModal && ( -
- + content={ (showModal || metric) && ( +
+
)} /> @@ -29,4 +43,7 @@ function CustomMetrics(props: Props) { ); } -export default CustomMetrics; \ No newline at end of file +export default connect(state => ({ + metric: state.getIn(['customMetrics', 'instance']), + alertInstance: state.getIn(['alerts', 'instance']), +}), { edit })(CustomMetrics); \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx b/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx index 69a1637d5..2fafe112f 100644 --- a/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx @@ -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 (
-
{ series.name }
- -
+ {/*
+ { series.name } +
+
*/} +
+ null } /> +
+ +
diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/SeriesName.tsx b/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/SeriesName.tsx new file mode 100644 index 000000000..7be696fd5 --- /dev/null +++ b/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/SeriesName.tsx @@ -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(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 ( +
+ setEditing(true)} + /> +
setEditing(true)}>
+
+ ); +} + +export default SeriesName; \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/index.ts b/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/index.ts new file mode 100644 index 000000000..90e63cdb6 --- /dev/null +++ b/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/index.ts @@ -0,0 +1 @@ +export { default } from './SeriesName'; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index fd215c11e..c313563ec 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -101,7 +101,7 @@ function FilterAutoComplete(props: Props) { setTimeout(() => { setShowModal(false) }, 10) } + onBlur={ () => setTimeout(() => { setShowModal(false) }, 50) } onFocus={ () => setShowModal(true)} value={ query } autoFocus={ true } diff --git a/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.css b/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.css new file mode 100644 index 000000000..c7a272458 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.css @@ -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; +} + diff --git a/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.js b/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.js new file mode 100644 index 000000000..8069bf3a8 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.js @@ -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 ( +
+ this.setState({ focused: true })} + onBlur={this.props.onBlur} + > + + + + this.setState({ focused: true })} + onBlur={this.props.onBlur} + > + + + +
+ ); + } +} diff --git a/frontend/app/components/shared/Filters/FilterDuration/index.js b/frontend/app/components/shared/Filters/FilterDuration/index.js new file mode 100644 index 000000000..cbf9296f3 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterDuration/index.js @@ -0,0 +1 @@ +export { default } from './FilterDuration'; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index c1e35f250..c45b7620a 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -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) {
{filterIndex+1}
-
- {filter.value && filter.value.map((value, valueIndex) => ( + {/*
*/} + {/* {filter.value && filter.value.map((value, valueIndex) => ( */} 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)} /> - ))} -
+ {/* ))} */} + {/*
*/}
void; className?: string; } function FilterOperator(props: Props) { const { filter, onChange, className = '' } = props; - const options = [] return ( } /> ); diff --git a/frontend/app/components/shared/Filters/FilterOperator/index.ts b/frontend/app/components/shared/Filters/FilterOperator/index.ts new file mode 100644 index 000000000..9345f24f8 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterOperator/index.ts @@ -0,0 +1 @@ +export { default } from './FilterOperator'; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 3483b3340..95ff92f71 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -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 ( + onSelect(e, { value }, valueIndex)} + /> + ) + case FilterType.DURATION: + return ( + + ) + case FilterType.NUMBER: + return ( + onSelect(e, { value: e.target.value }, valueIndex)} + /> + ) + case FilterType.MULTIPLE: + return ( + 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 ( - +
+ {filter.value && filter.value.map((value, valueIndex) => ( + renderValueFiled(value, valueIndex) + ))} +
); } diff --git a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.css b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.css new file mode 100644 index 000000000..8a2329dbd --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.css @@ -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; + } +} \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx new file mode 100644 index 000000000..55fc5ee38 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx @@ -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 ( + } + /> + ); +} + +export default FilterValueDropdown; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterValueDropdown/index.ts b/frontend/app/components/shared/Filters/FilterValueDropdown/index.ts new file mode 100644 index 000000000..0a0240086 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterValueDropdown/index.ts @@ -0,0 +1 @@ +export { default } from './FilterValueDropdown'; \ No newline at end of file diff --git a/frontend/app/components/ui/SegmentSelection/segmentSelection.css b/frontend/app/components/ui/SegmentSelection/segmentSelection.css index e33ec3b73..20007b010 100644 --- a/frontend/app/components/ui/SegmentSelection/segmentSelection.css +++ b/frontend/app/components/ui/SegmentSelection/segmentSelection.css @@ -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; diff --git a/frontend/app/duck/alerts.js b/frontend/app/duck/alerts.js index b93ff5878..1869db434 100644 --- a/frontend/app/duck/alerts.js +++ b/frontend/app/duck/alerts.js @@ -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; diff --git a/frontend/app/duck/customMetrics.js b/frontend/app/duck/customMetrics.js index f6a7b0b88..5842bb851 100644 --- a/frontend/app/duck/customMetrics.js +++ b/frontend/app/duck/customMetrics.js @@ -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, + }; } \ No newline at end of file diff --git a/frontend/app/types/customMetric.js b/frontend/app/types/customMetric.js index 1bfaebbd6..e40eddb27 100644 --- a/frontend/app/types/customMetric.js +++ b/frontend/app/types/customMetric.js @@ -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, diff --git a/frontend/app/types/dashboard/customMetric.js b/frontend/app/types/dashboard/customMetric.js new file mode 100644 index 000000000..9dd374b56 --- /dev/null +++ b/frontend/app/types/dashboard/customMetric.js @@ -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; \ No newline at end of file diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index e3663e860..c7ad79982 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -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; }); diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts new file mode 100644 index 000000000..c39cc1d2f --- /dev/null +++ b/frontend/app/types/filter/filterType.ts @@ -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", +} \ No newline at end of file diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 091b16277..40f3c406c 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -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: [""] // } -// } \ No newline at end of file +// } + + +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'; +} \ No newline at end of file diff --git a/frontend/env.js b/frontend/env.js index ad23c710a..549d5ec07 100644 --- a/frontend/env.js +++ b/frontend/env.js @@ -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,