From 155e7e43314d8c9346e47d1f031a20e89f51c10f Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 17 Jan 2022 17:06:01 +0530 Subject: [PATCH 1/6] feat(ui) - custom metrics --- frontend/app/api_client.js | 5 +- .../BugFinder/Attributes/AttributeItem.js | 7 +- .../Attributes/AttributeValueField.js | 36 +- .../BugFinder/AutoComplete/AutoComplete.js | 45 ++- .../BugFinder/AutoComplete/autoComplete.css | 34 ++ .../app/components/BugFinder/BugFinder.js | 12 +- .../BugFinder/EventFilter/EventFilter.js | 60 ++-- .../BugFinder/filterSelectionButton.css | 2 +- .../app/components/Dashboard/Dashboard.js | 2 + .../CustomMetricForm/CustomMetricForm.tsx | 118 +++++++ .../CustomMetrics/CustomMetricForm/index.ts | 1 + .../shared/CustomMetrics/CustomMetrics.tsx | 41 +++ .../FilterSeries/FilterSeries.tsx | 98 ++++++ .../CustomMetrics/FilterSeries/index.ts | 1 + .../components/shared/CustomMetrics/index.ts | 1 + .../Attributes/AttributeValueField.js | 6 +- .../EventFilter/AutoComplete/AutoComplete.js | 32 +- .../EventFilter/AutoComplete/autoComplete.css | 27 ++ .../EventSearchInput/EventSearchInput.tsx | 17 + .../shared/EventSearchInput/index.ts | 1 + .../FilterAutoComplete/FilterAutoComplete.css | 62 ++++ .../FilterAutoComplete/FilterAutoComplete.tsx | 147 +++++++++ .../Filters/FilterAutoComplete/index.ts | 1 + .../shared/Filters/FilterItem/FilterItem.tsx | 78 +++++ .../shared/Filters/FilterItem/index.ts | 1 + .../shared/Filters/FilterList/FilterList.tsx | 34 ++ .../shared/Filters/FilterList/index.ts | 1 + .../Filters/FilterModal/FilterModal.tsx | 34 ++ .../shared/Filters/FilterModal/index.ts | 1 + .../Filters/FilterOperator/FilterOperator.css | 19 ++ .../Filters/FilterOperator/FilterOperator.tsx | 29 ++ .../FilterSelection/FilterSelection.tsx | 42 +++ .../shared/Filters/FilterSelection/index.ts | 1 + .../Filters/FilterValue/FilterValue.tsx | 31 ++ .../shared/Filters/FilterValue/index.ts | 1 + .../SaveFilterButton/SaveFilterButton.tsx | 26 ++ .../shared/SaveFilterButton/index.ts | 1 + .../SaveSearchModal/SaveSearchModal.tsx | 78 +++++ .../shared/SaveSearchModal/index.ts | 1 + .../SaveSearchModal/saveSearchModal.css | 15 + .../shared/SavedSearch/SavedSearch.tsx | 47 +++ .../SavedSearchDropdown.css | 7 + .../SavedSearchDropdown.tsx | 24 ++ .../components/SavedSearchDropdown/index.ts | 1 + .../components/shared/SavedSearch/index.ts | 1 + .../SessionSearchField/SessionSearchField.css | 10 + .../SessionSearchField/SessionSearchField.tsx | 69 ++++ .../shared/SessionSearchField/index.ts | 1 + .../ui/SegmentSelection/SegmentSelection.js | 5 +- .../ui/SegmentSelection/segmentSelection.css | 5 + frontend/app/duck/customMetrics.js | 105 ++++++ frontend/app/duck/filters.js | 55 +++- frontend/app/duck/funnels.js | 3 +- frontend/app/duck/index.js | 2 + frontend/app/styles/main.css | 7 +- frontend/app/types/customMetric.js | 61 ++++ frontend/app/types/filter/customFilter.js | 30 +- frontend/app/types/filter/event.js | 36 +- frontend/app/types/filter/filter.js | 41 ++- frontend/app/types/filter/index.js | 28 +- frontend/app/types/filter/newFilter.js | 308 ++++++++++++++++++ frontend/env.js | 4 +- 62 files changed, 1914 insertions(+), 85 deletions(-) create mode 100644 frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx create mode 100644 frontend/app/components/shared/CustomMetrics/CustomMetricForm/index.ts create mode 100644 frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx create mode 100644 frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx create mode 100644 frontend/app/components/shared/CustomMetrics/FilterSeries/index.ts create mode 100644 frontend/app/components/shared/CustomMetrics/index.ts create mode 100644 frontend/app/components/shared/EventSearchInput/EventSearchInput.tsx create mode 100644 frontend/app/components/shared/EventSearchInput/index.ts create mode 100644 frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.css create mode 100644 frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx create mode 100644 frontend/app/components/shared/Filters/FilterAutoComplete/index.ts create mode 100644 frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx create mode 100644 frontend/app/components/shared/Filters/FilterItem/index.ts create mode 100644 frontend/app/components/shared/Filters/FilterList/FilterList.tsx create mode 100644 frontend/app/components/shared/Filters/FilterList/index.ts create mode 100644 frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx create mode 100644 frontend/app/components/shared/Filters/FilterModal/index.ts create mode 100644 frontend/app/components/shared/Filters/FilterOperator/FilterOperator.css create mode 100644 frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx create mode 100644 frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx create mode 100644 frontend/app/components/shared/Filters/FilterSelection/index.ts create mode 100644 frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx create mode 100644 frontend/app/components/shared/Filters/FilterValue/index.ts create mode 100644 frontend/app/components/shared/SaveFilterButton/SaveFilterButton.tsx create mode 100644 frontend/app/components/shared/SaveFilterButton/index.ts create mode 100644 frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx create mode 100644 frontend/app/components/shared/SaveSearchModal/index.ts create mode 100644 frontend/app/components/shared/SaveSearchModal/saveSearchModal.css create mode 100644 frontend/app/components/shared/SavedSearch/SavedSearch.tsx create mode 100644 frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.css create mode 100644 frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.tsx create mode 100644 frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/index.ts create mode 100644 frontend/app/components/shared/SavedSearch/index.ts create mode 100644 frontend/app/components/shared/SessionSearchField/SessionSearchField.css create mode 100644 frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx create mode 100644 frontend/app/components/shared/SessionSearchField/index.ts create mode 100644 frontend/app/duck/customMetrics.js create mode 100644 frontend/app/types/customMetric.js create mode 100644 frontend/app/types/filter/newFilter.js diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index 02e575033..28aa2913b 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -16,13 +16,14 @@ const siteIdRequiredPaths = [ '/integration/sources', '/issue_types', '/sample_rate', - '/flows', + '/saved_search', '/rehydrations', '/sourcemaps', '/errors', '/funnels', '/assist', - '/heatmaps' + '/heatmaps', + '/custom_metrics', ]; const noStoringFetchPathStarts = [ diff --git a/frontend/app/components/BugFinder/Attributes/AttributeItem.js b/frontend/app/components/BugFinder/Attributes/AttributeItem.js index bc4e45d41..ad27649dd 100644 --- a/frontend/app/components/BugFinder/Attributes/AttributeItem.js +++ b/frontend/app/components/BugFinder/Attributes/AttributeItem.js @@ -27,9 +27,9 @@ class AttributeItem extends React.PureComponent { applyFilter = debounce(this.props.applyFilter, 1000) fetchFilterOptionsDebounce = debounce(this.props.fetchFilterOptions, 500) - onFilterChange = (e, { name, value }) => { + onFilterChange = (name, value, valueIndex) => { const { index } = this.props; - this.props.editAttribute(index, name, value); + this.props.editAttribute(index, name, value, valueIndex); this.applyFilter(); } @@ -69,13 +69,14 @@ class AttributeItem extends React.PureComponent { /> } { - !filter.hasNoValue && + // !filter.hasNoValue && } diff --git a/frontend/app/components/BugFinder/Attributes/AttributeValueField.js b/frontend/app/components/BugFinder/Attributes/AttributeValueField.js index 440eadec0..9e6b7eb78 100644 --- a/frontend/app/components/BugFinder/Attributes/AttributeValueField.js +++ b/frontend/app/components/BugFinder/Attributes/AttributeValueField.js @@ -7,7 +7,7 @@ import { LinkStyledInput, CircularLoader } from 'UI'; import { KEYS } from 'Types/filter/customFilter'; import Event, { TYPES } from 'Types/filter/event'; import CustomFilter from 'Types/filter/customFilter'; -import { setActiveKey, addCustomFilter, removeCustomFilter, applyFilter } from 'Duck/filters'; +import { setActiveKey, addCustomFilter, removeCustomFilter, applyFilter, updateValue } from 'Duck/filters'; import DurationFilter from '../DurationFilter/DurationFilter'; import AutoComplete from '../AutoComplete'; @@ -24,6 +24,7 @@ const getHeader = (type) => { addCustomFilter, removeCustomFilter, applyFilter, + updateValue, }) class AttributeValueField extends React.PureComponent { state = { @@ -134,25 +135,46 @@ class AttributeValueField extends React.PureComponent { return params; } + onAddValue = () => { + const { index, filter } = this.props; + this.props.updateValue('filters', index, filter.value.concat("")); + } + + onRemoveValue = (valueIndex) => { + const { index, filter } = this.props; + this.props.updateValue('filters', index, filter.value.filter((_, i) => i !== valueIndex)); + } + + onChange = (name, value, valueIndex) => { + const { index, filter } = this.props; + this.props.updateValue('filters', index, filter.value.map((item, i) => i === valueIndex ? value : item)); + } + render() { - const { filter, onChange, onTargetChange } = this.props; + // const { filter, onChange } = this.props; + const { filter } = this.props; const _showAutoComplete = this.isAutoComplete(filter.type); const _params = _showAutoComplete ? this.getParams(filter) : {}; - let _optionsEndpoint= '/events/search'; + let _optionsEndpoint= '/events/search'; return ( - { _showAutoComplete ? - ( + onChange(name, value, i) } headerText={
{ getHeader(filter.type) }
} fullWidth={ (filter.type === TYPES.CONSOLE || filter.type === TYPES.LOCATION || filter.type === TYPES.CUSTOM) && filter.value } + onRemoveValue={() => this.onRemoveValue(i)} + onAddValue={this.onAddValue} + showCloseButton={i !== filter.value.length - 1} /> + )) : this.renderField() } { filter.type === 'INPUT' && diff --git a/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js b/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js index 7c22584a9..f902235ed 100644 --- a/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js +++ b/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js @@ -1,9 +1,10 @@ import React from 'react'; import APIClient from 'App/api_client'; import cn from 'classnames'; -import { Input } from 'UI'; +import { Input, Icon } from 'UI'; import { debounce } from 'App/utils'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; +import EventSearchInput from 'Shared/EventSearchInput'; import stl from './autoComplete.css'; import FilterItem from '../CustomFilters/FilterItem'; @@ -78,7 +79,7 @@ class AutoComplete extends React.PureComponent { }) - onInputChange = (e, { name, value }) => { + onInputChange = ({ target: { value } }) => { changed = true; this.setState({ query: value, updated: true }) const _value = value.trim(); @@ -118,23 +119,53 @@ class AutoComplete extends React.PureComponent { valueToText = defaultValueToText, placeholder = 'Type to search...', headerText = '', - fullWidth = false + fullWidth = false, + onRemoveValue = () => {}, + onAddValue = () => {}, + showCloseButton = false, } = this.props; const options = optionMapping(values, valueToText) return ( - */} +
+ this.setState({ddOpen: true})} + onChange={ this.onInputChange } + onBlur={ this.onBlur } + onFocus={ () => this.setState({ddOpen: true})} + value={ query } + autoFocus={ true } + type="text" + placeholder={ placeholder } + onPaste={(e) => { + const text = e.clipboardData.getData('Text'); + this.hiddenInput.value = text; + pasted = true; // to use only the hidden input + } } + /> +
+ { showCloseButton ? : or} +
+
+ + {showCloseButton &&
or
} + {/* this.setState({ddOpen: true})} value={ query } - icon="search" + // icon="search" + label={{ basic: true, content:
test
}} + labelPosition='right' loading={ loading } autoFocus={ true } type="search" @@ -144,7 +175,7 @@ class AutoComplete extends React.PureComponent { this.hiddenInput.value = text; pasted = true; // to use only the hidden input } } - /> + /> */} { ddOpen && options.length > 0 &&
diff --git a/frontend/app/components/BugFinder/AutoComplete/autoComplete.css b/frontend/app/components/BugFinder/AutoComplete/autoComplete.css index 79439f51c..09a9a6571 100644 --- a/frontend/app/components/BugFinder/AutoComplete/autoComplete.css +++ b/frontend/app/components/BugFinder/AutoComplete/autoComplete.css @@ -19,6 +19,13 @@ color: $gray-darkest !important; font-size: 14px !important; background-color: rgba(255, 255, 255, 0.8) !important; + + & .label { + padding: 0px !important; + display: flex; + align-items: center; + justify-content: center; + } } height: 28px !important; width: 280px; @@ -28,3 +35,30 @@ .fullWidth { width: 100% !important; } + +.inputWrapper { + border: solid thin $gray-light !important; + border-radius: 3px; + border-radius: 3px; + display: flex; + align-items: center; + & input { + height: 28px; + font-size: 13px !important; + padding: 0 5px !important; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + } + + & .right { + height: 28px; + display: flex; + align-items: center; + padding: 0 5px; + background-color: $gray-lightest; + border-left: solid thin $gray-light !important; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + cursor: pointer; + } +} \ No newline at end of file diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index a71c54cf9..4af7870f2 100644 --- a/frontend/app/components/BugFinder/BugFinder.js +++ b/frontend/app/components/BugFinder/BugFinder.js @@ -24,8 +24,10 @@ import SessionFlowList from './SessionFlowList/SessionFlowList'; import { LAST_7_DAYS } from 'Types/app/period'; import { resetFunnel } from 'Duck/funnels'; import { resetFunnelFilters } from 'Duck/funnelFilters' -import NoSessionsMessage from '../shared/NoSessionsMessage'; -import TrackerUpdateMessage from '../shared/TrackerUpdateMessage'; +import NoSessionsMessage from 'Shared/NoSessionsMessage'; +import TrackerUpdateMessage from 'Shared/TrackerUpdateMessage'; +import SessionSearchField from 'Shared/SessionSearchField' +import SavedSearch from 'Shared/SavedSearch' import LiveSessionList from './LiveSessionList' const weakEqual = (val1, val2) => { @@ -170,8 +172,12 @@ export default class BugFinder extends React.PureComponent { data-hidden={ activeTab === 'live' || activeTab === 'favorite' } className="mb-5" > +
+
+ +
-
+ { activeFlow && activeFlow.type === 'flows' && } { activeTab.type !== 'live' && } { activeTab.type === 'live' && } diff --git a/frontend/app/components/BugFinder/EventFilter/EventFilter.js b/frontend/app/components/BugFinder/EventFilter/EventFilter.js index 44fe0bf00..21467c382 100644 --- a/frontend/app/components/BugFinder/EventFilter/EventFilter.js +++ b/frontend/app/components/BugFinder/EventFilter/EventFilter.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { Input } from 'semantic-ui-react'; import { DNDContext } from 'Components/hocs/dnd'; import { - addEvent, applyFilter, moveEvent, clearEvents, + addEvent, applyFilter, moveEvent, clearEvents, edit, addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption } from 'Duck/filters'; import { fetchList as fetchEventList } from 'Duck/events'; @@ -11,7 +11,7 @@ import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; import EventEditor from './EventEditor'; import ListHeader from '../ListHeader'; import FilterModal from '../CustomFilters/FilterModal'; -import { IconButton } from 'UI'; +import { IconButton, SegmentSelection } from 'UI'; import stl from './eventFilter.css'; import Attributes from '../Attributes/Attributes'; import RandomPlaceholder from './RandomPlaceholder'; @@ -19,6 +19,7 @@ import CustomFilters from '../CustomFilters'; import ManageFilters from '../ManageFilters'; import { blink as setBlink } from 'Duck/funnels'; import cn from 'classnames'; +import SaveFilterButton from 'Shared/SaveFilterButton'; @connect(state => ({ events: state.getIn([ 'filters', 'appliedFilter', 'events' ]), @@ -41,7 +42,8 @@ import cn from 'classnames'; setSearchQuery, setActiveFlow, setFilterOption, - setBlink + setBlink, + edit, }) @DNDContext export default class EventFilter extends React.PureComponent { @@ -109,6 +111,10 @@ export default class EventFilter extends React.PureComponent { this.props.setActiveFlow(null) } + changeConditionTab = (e, { name, value }) => { + this.props.edit({ [ 'condition' ]: value }) + }; + render() { const { events, @@ -124,34 +130,6 @@ export default class EventFilter extends React.PureComponent { return ( - { showPlacehoder && !hasFilters && -
- { !searchQuery && -
Search for users, clicks, page visits, requests, errors and more
- // - } -
- } - - { hasFilters && -
+
+
+
Operator
+ +
+ { events.size > 0 && <>
@@ -189,6 +184,7 @@ export default class EventFilter extends React.PureComponent { showFilters={ true } />
+
diff --git a/frontend/app/components/BugFinder/filterSelectionButton.css b/frontend/app/components/BugFinder/filterSelectionButton.css index 1741e7d83..4c51cf3a4 100644 --- a/frontend/app/components/BugFinder/filterSelectionButton.css +++ b/frontend/app/components/BugFinder/filterSelectionButton.css @@ -10,7 +10,7 @@ width: 150px; color: $gray-darkest; cursor: pointer; - background-color: rgba(255, 255, 255, 0.8) !important; + background-color: rgba(0, 0, 0, 0.1) !important; &:hover { background-color: white; } diff --git a/frontend/app/components/Dashboard/Dashboard.js b/frontend/app/components/Dashboard/Dashboard.js index 2e5cba630..6af004136 100644 --- a/frontend/app/components/Dashboard/Dashboard.js +++ b/frontend/app/components/Dashboard/Dashboard.js @@ -5,6 +5,7 @@ import withPermissions from 'HOCs/withPermissions' import { setPeriod, setPlatform, fetchMetadataOptions } from 'Duck/dashboard'; import { NoContent } from 'UI'; import { WIDGET_KEYS } from 'Types/dashboard'; +import CustomMetrics from 'Shared/CustomMetrics'; import { MissingResources, @@ -184,6 +185,7 @@ export default class Dashboard extends React.PureComponent {
+
void; + save: (metric) => void; + loading: boolean; +} +function CustomMetricForm(props: Props) { + const { metric, loading } = props; + + const addSeries = () => { + const newSeries = { + name: `Series ${metric.series.size + 1}`, + type: '', + series: [], + filter: { + type: '', + value: '', + }, + }; + props.editMetric({ + ...metric, + series: [...metric.series, newSeries], + }); + } + + const removeSeries = (index) => { + const newSeries = metric.series.filter((_series, i) => { + return i !== index; + }); + props.editMetric({ + ...metric, + series: newSeries, + }); + } + + const write = ({ target: { value, name } }) => props.editMetric({ ...metric.toData(), [ name ]: value }) + + const changeConditionTab = (e, { name, value }) => { + props.editMetric({ ...metric.toData(), [ 'type' ]: value }) + }; + + return ( +
props.save(metric)} + > +
+
+ + +
+ +
+ +
+ Timeseries + of +
+ +
+
+
+ +
+ + {metric.series && metric.series.size > 0 && metric.series.map((series: any, index: number) => ( +
+ removeSeries(index)} + /> +
+ ))} +
+ + +
+ +
+ +
+
+ ); +} + +export default connect(state => ({ + metric: state.getIn(['customMetrics', 'instance']), + loading: state.getIn(['customMetrics', 'saveRequest', 'loading']), +}), { editMetric, save })(CustomMetricForm); \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/index.ts b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/index.ts new file mode 100644 index 000000000..e6ffb605b --- /dev/null +++ b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/index.ts @@ -0,0 +1 @@ +export { default } from './CustomMetricForm'; \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx new file mode 100644 index 000000000..ae7102202 --- /dev/null +++ b/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { IconButton, SlideModal } from 'UI' +import CustomMetricForm from './CustomMetricForm'; + +interface Props {} +function CustomMetrics(props: Props) { + return ( +
+ + + + { 'Custom Metric' } + {/* toggleForm({}, true) } + /> */} +
+ } + isDisplayed={ true } + // onClose={ () => { + // toggleForm({}, false); + // setShowAlerts(false); + // } } + // size="medium" + content={ +
+ +
+ } + /> +
+ ); +} + +export default 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 new file mode 100644 index 000000000..f29301644 --- /dev/null +++ b/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx @@ -0,0 +1,98 @@ +import React, { useState } from 'react'; +import FilterList from 'Shared/Filters/FilterList'; +import { edit, updateSeries } from 'Duck/customMetrics'; +import { connect } from 'react-redux'; +import { IconButton, Button, Icon } from 'UI'; +import FilterSelection from '../../Filters/FilterSelection'; + +interface Props { + seriesIndex: number; + series: any; + edit: typeof edit; + updateSeries: typeof updateSeries; + onRemoveSeries: (seriesIndex) => void; +} + +function FilterSeries(props: Props) { + const { series, seriesIndex } = props; + + const onAddFilter = (filter) => { + filter.value = [""] + const newFilters = series.filter.filters.concat(filter); + props.updateSeries(seriesIndex, { + ...series, + filter: { + ...series.filter, + filters: newFilters, + } + }); + } + + const onUpdateFilter = (filterIndex, filter) => { + const newFilters = series.filter.filters.map((_filter, i) => { + if (i === filterIndex) { + return filter; + } else { + return _filter; + } + }); + + props.updateSeries(seriesIndex, { + ...series.toData(), + filter: { + ...series.filter, + filters: newFilters, + } + }); + } + + const onRemoveFilter = (filterIndex) => { + const newFilters = series.filter.filters.filter((_filter, i) => { + return i !== filterIndex; + }); + + props.updateSeries(seriesIndex, { + ...series, + filter: { + ...series.filter, + filters: newFilters, + } + }); + } + + return ( +
+
+ { series.name } +
+ +
+
+
+ { series.filter.filters.size > 0 ? ( + + ): ( +
Add user event or filter to build the series.
+ )} +
+
+ + {/* */} + + +
+
+ ); +} + +export default connect(null, { edit, updateSeries })(FilterSeries); \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/index.ts b/frontend/app/components/shared/CustomMetrics/FilterSeries/index.ts new file mode 100644 index 000000000..5882e382a --- /dev/null +++ b/frontend/app/components/shared/CustomMetrics/FilterSeries/index.ts @@ -0,0 +1 @@ +export { default } from './FilterSeries' \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/index.ts b/frontend/app/components/shared/CustomMetrics/index.ts new file mode 100644 index 000000000..ebbb8203c --- /dev/null +++ b/frontend/app/components/shared/CustomMetrics/index.ts @@ -0,0 +1 @@ +export { default } from './CustomMetrics'; \ No newline at end of file diff --git a/frontend/app/components/shared/EventFilter/Attributes/AttributeValueField.js b/frontend/app/components/shared/EventFilter/Attributes/AttributeValueField.js index 176ae2c4a..daee15cc3 100644 --- a/frontend/app/components/shared/EventFilter/Attributes/AttributeValueField.js +++ b/frontend/app/components/shared/EventFilter/Attributes/AttributeValueField.js @@ -137,12 +137,13 @@ class AttributeValueField extends React.PureComponent { const { filter, onChange } = this.props; const _showAutoComplete = this.isAutoComplete(filter.type); const _params = _showAutoComplete ? this.getParams(filter) : {}; - let _optionsEndpoint= '/events/search'; + let _optionsEndpoint= '/events/search'; + console.log('value', filter.value) return ( { _showAutoComplete ? - { getHeader(filter.type) } } fullWidth={ (filter.type === TYPES.CONSOLE || filter.type === TYPES.LOCATION || filter.type === TYPES.CUSTOM) && filter.value } + // onAddOrRemove={} /> : this.renderField() } diff --git a/frontend/app/components/shared/EventFilter/AutoComplete/AutoComplete.js b/frontend/app/components/shared/EventFilter/AutoComplete/AutoComplete.js index 922b7f650..e075b17bd 100644 --- a/frontend/app/components/shared/EventFilter/AutoComplete/AutoComplete.js +++ b/frontend/app/components/shared/EventFilter/AutoComplete/AutoComplete.js @@ -77,7 +77,7 @@ class AutoComplete extends React.PureComponent { noResultsMessage: SOME_ERROR_MSG, }) - onInputChange = (e, { name, value }) => { + onInputChange = ({ target: { value } }) => { changed = true; this.setState({ query: value, updated: true }) const _value = value.trim(); @@ -118,7 +118,8 @@ class AutoComplete extends React.PureComponent { valueToText = defaultValueToText, placeholder = 'Type to search...', headerText = '', - fullWidth = false + fullWidth = false, + onAddOrRemove = () => null, } = this.props; const options = optionMapping(values, valueToText) @@ -128,7 +129,7 @@ class AutoComplete extends React.PureComponent { className={ cn("relative", { "flex-1" : fullWidth }) } onClickOutside={this.onClickOutside} > - + /> */} +
+ this.setState({ddOpen: true})} + onChange={ this.onInputChange } + onBlur={ this.onBlur } + onFocus={ () => this.setState({ddOpen: true})} + value={ query } + autoFocus={ true } + type="text" + placeholder={ placeholder } + onPaste={(e) => { + const text = e.clipboardData.getData('Text'); + this.hiddenInput.value = text; + pasted = true; // to use only the hidden input + } } + /> +
+ {/* */} + or +
+
{ ddOpen && options.length > 0 &&
diff --git a/frontend/app/components/shared/EventFilter/AutoComplete/autoComplete.css b/frontend/app/components/shared/EventFilter/AutoComplete/autoComplete.css index c2c827bfe..b72653c42 100644 --- a/frontend/app/components/shared/EventFilter/AutoComplete/autoComplete.css +++ b/frontend/app/components/shared/EventFilter/AutoComplete/autoComplete.css @@ -28,3 +28,30 @@ .fullWidth { width: 100% !important; } + +.inputWrapper { + border: solid thin $gray-light !important; + border-radius: 3px; + border-radius: 3px; + display: flex; + align-items: center; + & input { + height: 28px; + font-size: 13px !important; + padding: 0 5px !important; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + } + + & .right { + height: 28px; + display: flex; + align-items: center; + padding: 0 5px; + background-color: $gray-lightest; + border-left: solid thin $gray-light !important; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + cursor: pointer; + } +} \ No newline at end of file diff --git a/frontend/app/components/shared/EventSearchInput/EventSearchInput.tsx b/frontend/app/components/shared/EventSearchInput/EventSearchInput.tsx new file mode 100644 index 000000000..0a7752ccd --- /dev/null +++ b/frontend/app/components/shared/EventSearchInput/EventSearchInput.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +interface Props { + +} +function EventSearchInput(props) { + return ( +
+ +
+ ); +} + +export default EventSearchInput; \ No newline at end of file diff --git a/frontend/app/components/shared/EventSearchInput/index.ts b/frontend/app/components/shared/EventSearchInput/index.ts new file mode 100644 index 000000000..2e4e57078 --- /dev/null +++ b/frontend/app/components/shared/EventSearchInput/index.ts @@ -0,0 +1 @@ +export { default } from './EventSearchInput'; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.css b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.css new file mode 100644 index 000000000..ba7359239 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.css @@ -0,0 +1,62 @@ +.wrapper { + border: solid thin $gray-light !important; + border-radius: 3px; + border-radius: 3px; + display: flex; + align-items: center; + & input { + height: 28px; + font-size: 13px !important; + padding: 0 5px !important; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + border: solid thin transparent !important; + } + + & .right { + height: 28px; + display: flex; + align-items: center; + padding: 0 5px; + background-color: $gray-lightest; + border-left: solid thin $gray-light !important; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + cursor: pointer; + } +} + +.menu { + border-radius: 0 0 3px 3px; + box-shadow: 0 2px 10px 0 $gray-light; + padding: 20px; + background-color: white; + max-height: 350px; + overflow-y: auto; + position: absolute; + top: 28px; + left: 0; + width: 500px; + z-index: 99; +} + +.filterItem { + display: flex; + align-items: center; + padding: 8px; + cursor: pointer; + border-radius: 3px; + transition: all 0.4s; + margin-bottom: 5px; + max-width: 100%; + & .label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &:hover { + background-color: $gray-lightest; + transition: all 0.2s; + } +} \ 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 new file mode 100644 index 000000000..068b255f7 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -0,0 +1,147 @@ +import React, { useState, useEffect } from 'react'; +import { Icon, Loader } from 'UI'; +import APIClient from 'App/api_client'; +import { debounce } from 'App/utils'; +import stl from './FilterAutoComplete.css'; +import cn from 'classnames'; + +const hiddenStyle = { + whiteSpace: 'pre-wrap', + opacity: 0, position: 'fixed', left: '-3000px' +}; + +interface Props { + showOrButton?: boolean; + onRemoveValue?: () => void; + onAddValue?: () => void; + endpoint?: string; + method?: string; + params?: any; + headerText?: string; + placeholder?: string; + onSelect: (e, item) => void; + value: any; +} + +function FilterAutoComplete(props: Props) { + const { + placeholder = 'Type to search', + method = 'GET', + showOrButton = false, + onRemoveValue = () => null, + onAddValue = () => null, + endpoint = '', + params = {}, + headerText = '', + value = '', + } = props; + const [showModal, setShowModal] = useState(true) + const [loading, setLoading] = useState(false) + const [options, setOptions] = useState([]); + const [query, setQuery] = useState(value); + + + const requestValues = (q) => { + // const { params, method } = props; + setLoading(true); + + return new APIClient()[method?.toLowerCase()](endpoint, { ...params, q }) + .then(response => response.json()) + .then(({ errors, data }) => { + if (errors) { + // this.setError(); + } else { + setOptions(data); + // this.setState({ + // ddOpen: true, + // values: data, + // loading: false, + // noResultsMessage: NO_RESULTS_MSG, + // }); + } + }).finally(() => setLoading(false)); + // .catch(this.setError); + } + + const debouncedRequestValues = debounce(requestValues, 1000) + + const onInputChange = ({ target: { value } }) => { + setQuery(value); + } + + useEffect(() => { + if (query === '' || query === ' ') { + return + } + + debouncedRequestValues(query) + }, [query]) + + const onItemClick = (e, item) => { + e.stopPropagation(); + e.preventDefault(); + // const { onSelect, name } = this.props; + + + if (query !== item.value) { + setQuery(item.value); + } + // this.setState({ query: item.value, ddOpen: false}) + props.onSelect(e, item); + // setTimeout(() => { + // setShowModal(false) + // }, 10) + } + + return ( +
+
+ setTimeout(() => { setShowModal(false) }, 10) } + onFocus={ () => setShowModal(true)} + value={ query } + autoFocus={ true } + type="text" + placeholder={ placeholder } + // onPaste={(e) => { + // const text = e.clipboardData.getData('Text'); + // // this.hiddenInput.value = text; + // // pasted = true; // to use only the hidden input + // } } + /> +
+ { !showOrButton && } + { showOrButton && or} +
+
+ + {/* */} + + { showModal && (options.length > 0 || loading) && +
+ { headerText && headerText } + + { + options.map(item => ( +
onItemClick(e, item) } + > + { item.icon && } + { item.value } +
+ )) + } +
+
+ } +
+ ); +} + +export default FilterAutoComplete; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/index.ts b/frontend/app/components/shared/Filters/FilterAutoComplete/index.ts new file mode 100644 index 000000000..8540e6f40 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/index.ts @@ -0,0 +1 @@ +export { default } from './FilterAutoComplete'; \ 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 new file mode 100644 index 000000000..66aa08ff3 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import FilterOperator from '../FilterOperator/FilterOperator'; +import FilterSelection from '../FilterSelection'; +import FilterValue from '../FilterValue'; +import { Icon } from 'UI'; + +interface Props { + filterIndex: number; + filter: any; // event/filter + onUpdate: (filter) => void; + onRemoveFilter: () => void; +} +function FitlerItem(props: Props) { + const { filterIndex, filter, onUpdate } = props; + + const replaceFilter = (filter) => { + onUpdate(filter); + }; + + 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 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); + + const onOperatorChange = (e, { name, value }) => { + onUpdate({ ...filter, operator: value }) + } + + return ( +
+
+
{filterIndex+1}
+ + +
+ {filter.value && filter.value.map((value, valueIndex) => ( + onRemoveValue(valueIndex)} + onSelect={(e, item) => onSelect(e, valueIndex, item)} + /> + ))} +
+
+
+
+ +
+
+
+ ); +} + +export default FitlerItem; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterItem/index.ts b/frontend/app/components/shared/Filters/FilterItem/index.ts new file mode 100644 index 000000000..b09a3e2f1 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterItem/index.ts @@ -0,0 +1 @@ +export { default } from './FilterItem'; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx new file mode 100644 index 000000000..d2e20cff6 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx @@ -0,0 +1,34 @@ +import React, { useState} from 'react'; +import FilterItem from '../FilterItem'; + +interface Props { + filters: any[]; // event/filter + onUpdateFilter: (filterIndex, filter) => void; + onRemoveFilter: (filterIndex) => void; +} +function FilterList(props: Props) { + const { filters } = props; + + const onRemoveFilter = (filterIndex) => { + const newFilters = filters.filter((_filter, i) => { + return i !== filterIndex; + }); + + props.onRemoveFilter(filterIndex); + } + + return ( +
+ {filters.map((filter, filterIndex) => ( + props.onUpdateFilter(filterIndex, filter)} + onRemoveFilter={() => onRemoveFilter(filterIndex) } + /> + ))} +
+ ); +} + +export default FilterList; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterList/index.ts b/frontend/app/components/shared/Filters/FilterList/index.ts new file mode 100644 index 000000000..ecf0adf70 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterList/index.ts @@ -0,0 +1 @@ +export { default } from './FilterList'; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx new file mode 100644 index 000000000..4a16b6511 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Icon } from 'UI'; +import { connect } from 'react-redux'; + +interface Props { + filters: any, + onFilterClick?: (filter) => void +} +function FilterModal(props: Props) { + const { filters, onFilterClick = () => null } = props; + return ( +
+
+ {filters && Object.keys(filters).map((key) => ( +
+
{key}
+
+ {filters[key].map((filter: any) => ( +
onFilterClick(filter)}> + + {filter.label} +
+ ))} +
+
+ ))} +
+
+ ); +} + +export default connect(state => ({ + filters: state.getIn([ 'filters', 'filterList' ]) +}))(FilterModal); \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterModal/index.ts b/frontend/app/components/shared/Filters/FilterModal/index.ts new file mode 100644 index 000000000..a8ab8d552 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterModal/index.ts @@ -0,0 +1 @@ +export { default } from './FilterModal'; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.css b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.css new file mode 100644 index 000000000..8a2329dbd --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.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/FilterOperator/FilterOperator.tsx b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx new file mode 100644 index 000000000..8cb6d6bf0 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import cn from 'classnames'; +import { Dropdown, Icon } from 'UI'; +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 ( + } + /> + ); +} + +export default FilterOperator; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx new file mode 100644 index 000000000..e899f6566 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; +import FilterModal from '../FilterModal'; +import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; +import { Icon } from 'UI'; + +interface Props { + filter: any; // event/filter + onFilterClick: (filter) => void; + children?: any; +} +function FilterSelection(props: Props) { + const { filter, onFilterClick, children } = props; + const [showModal, setShowModal] = useState(false) + return ( +
+ setTimeout(function() { + setShowModal(false) + }, 20)} + > + { children ? React.cloneElement(children, { onClick: () => setShowModal(true)}) : ( +
setShowModal(true)} + > + {filter.label} + +
+ ) } +
+ {showModal && ( +
+ +
+ )} +
+ ); +} + +export default FilterSelection; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterSelection/index.ts b/frontend/app/components/shared/Filters/FilterSelection/index.ts new file mode 100644 index 000000000..8c9764781 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterSelection/index.ts @@ -0,0 +1 @@ +export { default } from './FilterSelection'; \ 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 new file mode 100644 index 000000000..65e123ba4 --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import FilterAutoComplete from '../FilterAutoComplete'; + +interface Props { + index: number; + value: any; // event/filter + onRemoveValue?: () => void; + onAddValue?: () => void; + showOrButton: boolean; + onSelect: (e, item) => void; +} +function FilterValue(props: Props) { + const { index, value, showOrButton, onRemoveValue , onAddValue } = props; + + return ( + + ); +} + +export default FilterValue; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterValue/index.ts b/frontend/app/components/shared/Filters/FilterValue/index.ts new file mode 100644 index 000000000..a4e4a517e --- /dev/null +++ b/frontend/app/components/shared/Filters/FilterValue/index.ts @@ -0,0 +1 @@ +export { default } from './FilterValue'; \ No newline at end of file diff --git a/frontend/app/components/shared/SaveFilterButton/SaveFilterButton.tsx b/frontend/app/components/shared/SaveFilterButton/SaveFilterButton.tsx new file mode 100644 index 000000000..82be6101a --- /dev/null +++ b/frontend/app/components/shared/SaveFilterButton/SaveFilterButton.tsx @@ -0,0 +1,26 @@ +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import { save } from 'Duck/filters'; +import { Button } from 'UI'; +import SaveSearchModal from 'Shared/SaveSearchModal' + +interface Props { + filter: any; +} + +function SaveFilterButton(props) { + const [showModal, setshowModal] = useState(false) + return ( +
+ + setshowModal(false)} + /> +
+ ); +} + +export default connect(state => ({ + filter: state.getIn([ 'filters', 'appliedFilter' ]), +}), { save })(SaveFilterButton); \ No newline at end of file diff --git a/frontend/app/components/shared/SaveFilterButton/index.ts b/frontend/app/components/shared/SaveFilterButton/index.ts new file mode 100644 index 000000000..9f22c4ecb --- /dev/null +++ b/frontend/app/components/shared/SaveFilterButton/index.ts @@ -0,0 +1 @@ +export { default } from './SaveFilterButton' \ No newline at end of file diff --git a/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx b/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx new file mode 100644 index 000000000..e609440c2 --- /dev/null +++ b/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { edit, save } from 'Duck/filters'; +import { Button, Modal, Form, Icon, Checkbox } from 'UI'; +import stl from './SaveSearchModal.css'; + +interface Props { + filter: any; + loading: boolean; + edit: (filter: any) => void; + save: (filter: any) => Promise; + show: boolean; + closeHandler: () => void; +} +function SaveSearchModal(props: Props) { + const { filter, loading, show, closeHandler } = props; + + const onNameChange = ({ target: { value } }) => { + props.edit({ name: value }); + }; + + const onSave = () => { + const { filter, closeHandler } = props; + if (filter.name.trim() === '') return; + props.save(filter).then(function() { + // this.props.fetchFunnelsList(); + closeHandler(); + }); + } + console.log('filter', filter); + return ( + + +
{ 'Save Search' }
+ +
+ + +
+ + + + +
+
+ + + + +
+ ); +} + +export default connect(state => ({ + filter: state.getIn(['filters', 'instance']), + loading: state.getIn([ 'filters', 'saveRequest', 'loading' ]) || + state.getIn([ 'filters', 'updateRequest', 'loading' ]), +}), { edit, save })(SaveSearchModal); \ No newline at end of file diff --git a/frontend/app/components/shared/SaveSearchModal/index.ts b/frontend/app/components/shared/SaveSearchModal/index.ts new file mode 100644 index 000000000..6c5515e82 --- /dev/null +++ b/frontend/app/components/shared/SaveSearchModal/index.ts @@ -0,0 +1 @@ +export { default } from './SaveSearchModal' \ No newline at end of file diff --git a/frontend/app/components/shared/SaveSearchModal/saveSearchModal.css b/frontend/app/components/shared/SaveSearchModal/saveSearchModal.css new file mode 100644 index 000000000..ed2600745 --- /dev/null +++ b/frontend/app/components/shared/SaveSearchModal/saveSearchModal.css @@ -0,0 +1,15 @@ +@import 'mixins.css'; + +.modalHeader { + display: flex !important; + align-items: center; + justify-content: space-between; +} + +.cancelButton { + @mixin plainButton; +} + +.applyButton { + @mixin basicButton; +} \ No newline at end of file diff --git a/frontend/app/components/shared/SavedSearch/SavedSearch.tsx b/frontend/app/components/shared/SavedSearch/SavedSearch.tsx new file mode 100644 index 000000000..170e90004 --- /dev/null +++ b/frontend/app/components/shared/SavedSearch/SavedSearch.tsx @@ -0,0 +1,47 @@ +import React, { useState, useEffect } from 'react'; +import { Button, Icon } from 'UI'; +import SavedSearchDropdown from './components/SavedSearchDropdown'; +import { connect } from 'react-redux'; +import { fetchList as fetchListSavedSearch } from 'Duck/filters'; +import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; + +interface Props { + fetchListSavedSearch: () => void; + list: any; +} +function SavedSearch(props) { + const [showMenu, setShowMenu] = useState(false) + + useEffect(() => { + props.fetchListSavedSearch() + }, []) + + return ( + setShowMenu(false)} + > +
+ + { showMenu && ( +
+ +
+ )} +
+
+ ); +} + +export default connect(state => ({ + list: state.getIn([ 'filters', 'list' ]), +}), { fetchListSavedSearch })(SavedSearch); \ No newline at end of file diff --git a/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.css b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.css new file mode 100644 index 000000000..96609ccae --- /dev/null +++ b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.css @@ -0,0 +1,7 @@ +.wrapper { + position: relative; + display: inline-block; + z-index: 999; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.tsx b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.tsx new file mode 100644 index 000000000..92746e78d --- /dev/null +++ b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import stl from './SavedSearchDropdown.css'; + +interface Props { + list: Array +} + +function Row ({ name }) { + return ( +
{name}
+ ) +} + +function SavedSearchDropdown(props: Props) { + return ( +
+ {props.list.map(item => ( + + ))} +
+ ); +} + +export default SavedSearchDropdown; \ No newline at end of file diff --git a/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/index.ts b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/index.ts new file mode 100644 index 000000000..2fea67949 --- /dev/null +++ b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/index.ts @@ -0,0 +1 @@ +export { default } from './SavedSearchDropdown'; \ No newline at end of file diff --git a/frontend/app/components/shared/SavedSearch/index.ts b/frontend/app/components/shared/SavedSearch/index.ts new file mode 100644 index 000000000..71c14305d --- /dev/null +++ b/frontend/app/components/shared/SavedSearch/index.ts @@ -0,0 +1 @@ +export { default } from './SavedSearch' \ No newline at end of file diff --git a/frontend/app/components/shared/SessionSearchField/SessionSearchField.css b/frontend/app/components/shared/SessionSearchField/SessionSearchField.css new file mode 100644 index 000000000..6a3a268ba --- /dev/null +++ b/frontend/app/components/shared/SessionSearchField/SessionSearchField.css @@ -0,0 +1,10 @@ +.searchField { + box-shadow: none !important; + & input { + box-shadow: none !important; + border-radius: 3 !important; + border: solid thin $gray-light !important; + height: 34px !important; + font-size: 16px; + } +} \ No newline at end of file diff --git a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx new file mode 100644 index 000000000..76aac5240 --- /dev/null +++ b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx @@ -0,0 +1,69 @@ +import React, { useRef, useState } from 'react'; +import { connect } from 'react-redux'; +import stl from './SessionSearchField.css'; +import { Input } from 'UI'; +import FilterModal from 'Shared/EventFilter/FilterModal'; +import { fetchList as fetchEventList } from 'Duck/events'; +import { debounce } from 'App/utils'; +import { + addEvent, applyFilter, moveEvent, clearEvents, + addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption +} from 'Duck/filters'; + +interface Props { + setSearchQuery: (query: string) => void; + fetchEventList: (query: any) => void; + searchQuery: string +} +function SessionSearchField(props: Props) { + const debounceFetchEventList = debounce(props.fetchEventList, 1000) + const [showModal, setShowModal] = useState(false) + + const onSearchChange = (e, { value }) => { + // props.setSearchQuery(value) + debounceFetchEventList({ q: value }); + } + + return ( +
+ setShowModal(true) } + onBlur={ () => setTimeout(setShowModal, 100, false) } + // ref={ this.inputRef } + onChange={ onSearchChange } + // onKeyUp={this.onKeyUp} + // value={props.searchQuery} + icon="search" + iconPosition="left" + placeholder={ 'Search sessions using any captured event (click, input, page, error...)'} + fluid + id="search" + type="search" + autocomplete="off" + /> + + setShowModal(false) } + displayed={ showModal } + // displayed={ true } + // loading={ loading } + // searchedEvents={ searchedEvents } + searchQuery={ props.searchQuery } + /> +
+ ); +} + +export default connect(state => ({ + events: state.getIn([ 'filters', 'appliedFilter', 'events' ]), + appliedFilter: state.getIn([ 'filters', 'appliedFilter' ]), + searchQuery: state.getIn([ 'filters', 'searchQuery' ]), + appliedFilterKeys: state.getIn([ 'filters', 'appliedFilter', 'filters' ]) + .map(({type}) => type).toJS(), + searchedEvents: state.getIn([ 'events', 'list' ]), + loading: state.getIn([ 'events', 'loading' ]), + strict: state.getIn([ 'filters', 'appliedFilter', 'strict' ]), + blink: state.getIn([ 'funnels', 'blink' ]), +}), { setSearchQuery, fetchEventList })(SessionSearchField); \ No newline at end of file diff --git a/frontend/app/components/shared/SessionSearchField/index.ts b/frontend/app/components/shared/SessionSearchField/index.ts new file mode 100644 index 000000000..1f99e0c0b --- /dev/null +++ b/frontend/app/components/shared/SessionSearchField/index.ts @@ -0,0 +1 @@ +export { default } from './SessionSearchField'; \ No newline at end of file diff --git a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js b/frontend/app/components/ui/SegmentSelection/SegmentSelection.js index c4d71a93a..d1d3ae6df 100644 --- a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js +++ b/frontend/app/components/ui/SegmentSelection/SegmentSelection.js @@ -9,12 +9,13 @@ class SegmentSelection extends React.Component { } render() { - const { className, list, primary = false, size = "normal" } = this.props; + const { className, list, small = false, extraSmall = false, primary = false, size = "normal" } = this.props; return (
{ list.map(item => ( diff --git a/frontend/app/components/ui/SegmentSelection/segmentSelection.css b/frontend/app/components/ui/SegmentSelection/segmentSelection.css index 543016246..a74d7d77b 100644 --- a/frontend/app/components/ui/SegmentSelection/segmentSelection.css +++ b/frontend/app/components/ui/SegmentSelection/segmentSelection.css @@ -61,4 +61,9 @@ .small .item { padding: 4px 8px; +} + +.extraSmall .item { + padding: 2px 4px; + font-size: 12px; } \ No newline at end of file diff --git a/frontend/app/duck/customMetrics.js b/frontend/app/duck/customMetrics.js new file mode 100644 index 000000000..0909ff3b1 --- /dev/null +++ b/frontend/app/duck/customMetrics.js @@ -0,0 +1,105 @@ +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 { createEdit, createInit } from './funcTools/crud'; +import { createRequestReducer, ROOT_KEY } from './funcTools/request'; +import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools'; +import Filter from 'Types/filter'; +import NewFilter from 'Types/filter/newFilter'; +import Event from 'Types/filter/event'; +// import CustomFilter from 'Types/filter/customFilter'; + +const name = "custom_metric"; +const idKey = "metricId"; + +const FETCH_LIST = fetchListType(name); +const FETCH = fetchType(name); +const SAVE = saveType(name); +const EDIT = editType(name); +const UPDATE_SERIES = `${name}/UPDATE_SERIES`; + +function chartWrapper(chart = []) { + return chart.map(point => ({ ...point, count: Math.max(point.count, 0) })); +} + +// const updateItemInList = createListUpdater(idKey); +// const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ] +// ? state.mergeIn([ "instance" ], instance) +// : state; + +const initialState = Map({ + list: List(), + instance: CustomMetric({ + name: 'New', + series: List([ + { + name: 'Session Count', + filter: new Filter({ filters: [] }), + }, + ]) + }), +}); + +// Metric - Series - [] - filters +function reducer(state = initialState, action = {}) { + switch (action.type) { + case EDIT: + return state.mergeIn([ 'instance' ], CustomMetric(action.instance)); + case UPDATE_SERIES: + return state.setIn(['instance', 'series', action.index], FilterSeries(action.series)); + case success(SAVE): + return state.set([ 'instance' ], CustomMetric(action.data)); + case success(FETCH): + return state.set("instance", ErrorInfo(action.data)); + case success(FETCH_LIST): + const { data } = action; + return state + .set("totalCount", data ? data.total : 0) + .set("list", List(data && data.errors).map(CustomMetric) + .filter(e => e.parentErrorId == null) + .map(e => e.update("chart", chartWrapper))); + } + return state; +} + +export default mergeReducers( + reducer, + createRequestReducer({ + [ ROOT_KEY ]: FETCH_LIST, + fetch: FETCH, + }), +); + +export const edit = createEdit(name); + +export const updateSeries = (index, series) => ({ + type: UPDATE_SERIES, + index, + series, +}); + +export function fetch(id) { + return { + id, + types: array(FETCH), + call: c => c.get(`/errors/${id}`), + } +} + +export function save(instance) { + return { + types: SAVE.array, + call: client => client.post( `/${ name }s`, instance.toData()), + }; +} + +export function fetchList(params = {}, clear = false) { + return { + types: array(FETCH_LIST), + call: client => client.post('/errors/search', params), + clear, + params: cleanParams(params), + }; +} \ No newline at end of file diff --git a/frontend/app/duck/filters.js b/frontend/app/duck/filters.js index a06a0b6bc..290a4f1cb 100644 --- a/frontend/app/duck/filters.js +++ b/frontend/app/duck/filters.js @@ -7,8 +7,33 @@ import CustomFilter, { KEYS } from 'Types/filter/customFilter'; import withRequestState, { RequestTypes } from './requestStateCreator'; import { fetchList as fetchSessionList } from './sessions'; import { fetchList as fetchErrorsList } from './errors'; +import { fetchListType, fetchType, saveType, editType, initType, removeType } from './funcTools/crud/types'; import logger from 'App/logger'; +import { newFiltersList } from 'Types/filter' +import NewFilter, { filtersMap } from 'Types/filter/newFilter'; + +const filterOptions = {} +// newFiltersList.forEach(filter => { +// filterOptions[filter.category] = filter +// }) + +Object.keys(filtersMap).forEach(key => { + const filter = filtersMap[key]; + if (filterOptions.hasOwnProperty(filter.category)) { + filterOptions[filter.category].push(filter); + } else { + filterOptions[filter.category] = [filter]; + } +}) + +console.log('filterOptions', filterOptions) + + +// for (var i = 0; i < newFiltersList.length; i++) { +// filterOptions[newFiltersList[i].category] = newFiltersList.filter(filter => filter.category === newFiltersList[i].category) +// } + const ERRORS_ROUTE = errorsRoute(); const FETCH_LIST = new RequestTypes('filters/FETCH_LIST'); @@ -16,6 +41,7 @@ const FETCH_FILTER_OPTIONS = new RequestTypes('filters/FETCH_FILTER_OPTIONS'); const SET_FILTER_OPTIONS = 'filters/SET_FILTER_OPTIONS'; const SAVE = new RequestTypes('filters/SAVE'); const REMOVE = new RequestTypes('filters/REMOVE'); +const EDIT = editType('funnel/EDIT'); const SET_SEARCH_QUERY = 'filters/SET_SEARCH_QUERY'; const SET_ACTIVE = 'filters/SET_ACTIVE'; @@ -35,7 +61,11 @@ const EDIT_ATTRIBUTE = 'filters/EDIT_ATTRIBUTE'; const REMOVE_ATTRIBUTE = 'filters/REMOVE_ATTRIBUTE'; const SET_ACTIVE_FLOW = 'filters/SET_ACTIVE_FLOW'; +const UPDATE_VALUE = 'filters/UPDATE_VALUE'; + const initialState = Map({ + filterList: filterOptions, + instance: Filter(), activeFilter: null, list: List(), appliedFilter: Filter(), @@ -71,6 +101,8 @@ const updateList = (state, instance) => state.update('list', (list) => { const reducer = (state = initialState, action = {}) => { let optionsMap = null; switch (action.type) { + case EDIT: + return state.mergeIn([ 'appliedFilter' ], action.instance); case FETCH_FILTER_OPTIONS.SUCCESS: optionsMap = state.getIn(['filterOptions', action.key]).map(i => i.value).toJS(); return state.mergeIn(['filterOptions', action.key], Set(action.data.filter(i => !optionsMap.includes(i.value)))); @@ -177,6 +209,8 @@ const reducer = (state = initialState, action = {}) => { return state.removeIn([ 'appliedFilter', 'filters', action.index ]); case SET_SEARCH_QUERY: return state.set('searchQuery', action.query); + case UPDATE_VALUE: + return state.setIn([ 'appliedFilter', action.filterType, action.index, 'value' ], action.value); default: return state; } @@ -234,7 +268,7 @@ export function removeAttribute(index) { export function fetchList(range) { return { types: FETCH_LIST.toArray(), - call: client => client.get(`/flows${range ? '?range_value=' + range : ''}`), + call: client => client.get(`/saved_search`), }; } @@ -257,7 +291,7 @@ export function setFilterOption(key, filterOption) { export function save(instance) { return { types: SAVE.toArray(), - call: client => client.post('/filters', instance.toData()), + call: client => client.post('/saved_search', instance.toData()), instance, }; } @@ -367,4 +401,21 @@ export function setSearchQuery(query) { type: SET_SEARCH_QUERY, query } +} + +export const edit = instance => { + return { + type: EDIT, + instance, + } +}; + +// filterType: 'events' or 'filters' +export const updateValue = (filterType, index, value) => { + return { + type: UPDATE_VALUE, + filterType, + index, + value + } } \ No newline at end of file diff --git a/frontend/app/duck/funnels.js b/frontend/app/duck/funnels.js index 4e591237c..7a611dbca 100644 --- a/frontend/app/duck/funnels.js +++ b/frontend/app/duck/funnels.js @@ -117,9 +117,8 @@ const reducer = (state = initialState, action = {}) => { .set('issueTypesMap', tmpMap); case FETCH_INSIGHTS_SUCCESS: let stages = []; - if (action.isRefresh) { + if (action.isRefresh) { const activeStages = state.get('activeStages'); - console.log('test', activeStages); const oldInsights = state.get('insights'); const lastStage = action.data.stages[action.data.stages.length - 1] const lastStageIndex = activeStages.toJS()[1]; diff --git a/frontend/app/duck/index.js b/frontend/app/duck/index.js index c8d7a7c65..71eb6d0e2 100644 --- a/frontend/app/duck/index.js +++ b/frontend/app/duck/index.js @@ -34,6 +34,7 @@ import errors from './errors'; import funnels from './funnels'; import config from './config'; import roles from './roles'; +import customMetrics from './customMetrics'; export default combineReducers({ jwt, @@ -68,6 +69,7 @@ export default combineReducers({ funnels, config, roles, + customMetrics, ...integrations, ...sources, }); diff --git a/frontend/app/styles/main.css b/frontend/app/styles/main.css index c77533f3c..077ec9904 100644 --- a/frontend/app/styles/main.css +++ b/frontend/app/styles/main.css @@ -106,4 +106,9 @@ opacity: .5; } - +.form-group { + margin-bottom: 20px; + & label { + margin-bottom: 5px; + } +} \ No newline at end of file diff --git a/frontend/app/types/customMetric.js b/frontend/app/types/customMetric.js new file mode 100644 index 000000000..32c4386b6 --- /dev/null +++ b/frontend/app/types/customMetric.js @@ -0,0 +1,61 @@ +import Record from 'Types/Record'; +import { List } from 'immutable'; +// import { DateTime } from 'luxon'; +// import { validateEmail, validateName } from 'App/validate'; +import Filter from 'Types/filter'; +import NewFilter from 'Types/filter'; +// import { Event } from 'Types/filter'; +// import CustomFilter from 'Types/filter/customFilter'; + +export const FilterSeries = Record({ + seriesId: undefined, + index: undefined, + name: 'Filter Series', + filter: new Filter(), +}, { + idKey: 'seriesId', + methods: { + toData() { + const js = this.toJS(); + // js.filter = js.filter.toData(); + return js; + }, + }, + fromJS: ({ filter, ...rest }) => ({ + ...rest, + filter: new Filter(filter), + }), +}); + +export default Record({ + metricId: undefined, + name: 'Series', + type: 'session_count', + series: List(), + isPublic: false, +}, { + idKey: 'metricId', + methods: { + validate() { + return validateName(this.name, { diacritics: true }); + }, + + toData() { + const js = this.toJS(); + js.series = js.series.map(series => { + series.filter.filters = series.filter.filters.map(filter => { + delete filter.operatorOptions + delete filter.icon + return filter; + }); + return series; + }); + + return js; + }, + }, + fromJS: ({ series, ...rest }) => ({ + ...rest, + series: List(series).map(FilterSeries), + }), +}); diff --git a/frontend/app/types/filter/customFilter.js b/frontend/app/types/filter/customFilter.js index ea7d82af2..59e47d985 100644 --- a/frontend/app/types/filter/customFilter.js +++ b/frontend/app/types/filter/customFilter.js @@ -30,6 +30,20 @@ const REVID = 'REVID'; const USERANONYMOUSID = 'USERANONYMOUSID'; 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'; + + +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 KEYS = { ERROR, MISSING_RESOURCE, @@ -56,7 +70,19 @@ export const KEYS = { STATEACTION, REVID, USERANONYMOUSID, - USERID + 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, }; const getOperatorDefault = (type) => { @@ -85,7 +111,7 @@ export default Record({ label: '', icon: '', type: '', - value: '', + value: [""], custom: '', target: Target(), level: '', diff --git a/frontend/app/types/filter/event.js b/frontend/app/types/filter/event.js index 2a6f54da6..17d6357ff 100644 --- a/frontend/app/types/filter/event.js +++ b/frontend/app/types/filter/event.js @@ -17,6 +17,13 @@ const USERANONYMOUSID = 'USERANONYMOUSID'; const USERID = 'USERID'; const ERROR = 'ERROR'; +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 TYPES = { CONSOLE, GRAPHQL, @@ -31,7 +38,14 @@ export const TYPES = { METADATA, USERANONYMOUSID, USERID, - ERROR + ERROR, + + DOM_COMPLETE, + LARGEST_CONTENTFUL_PAINT_TIME, + TIME_BETWEEN_EVENTS, + TTFB, + AVG_CPU_LOAD, + AVG_MEMORY_USAGE, }; function getLabelText(type, source) { @@ -49,11 +63,19 @@ function getLabelText(type, source) { if (type === TYPES.USERID) return 'User Id'; if (type === TYPES.USERANONYMOUSID) return 'User Anonymous Id'; if (type === TYPES.REVID) return 'Rev ID'; + + if (type === TYPES.DOM_COMPLETE) return 'DOM Complete'; + if (type === TYPES.LARGEST_CONTENTFUL_PAINT_TIME) return 'Largest Contentful Paint Time'; + if (type === TYPES.TIME_BETWEEN_EVENTS) return 'Time Between Events'; + if (type === TYPES.TTFB) return 'TTFB'; + if (type === TYPES.AVG_CPU_LOAD) return 'Avg CPU Load'; + if (type === TYPES.AVG_MEMORY_USAGE) return 'Avg Memory Usage'; + if (type === TYPES.CUSTOM) { if (!source) return 'Custom'; return source; } - return '?'; + return type; } export default Record({ @@ -63,7 +85,7 @@ export default Record({ operator: 'on', type: '', searchType: '', - value: '', + value: [], custom: '', inputValue: '', target: Target(), @@ -73,16 +95,16 @@ export default Record({ }, { keyKey: "_key", fromJS: ({ target, type = "" , ...event }) => { - const viewType = type.split('_')[0]; + // const viewType = type.split('_')[0]; return { ...event, - type: viewType, + type: type, searchType: event.searchType || type, - label: getLabelText(viewType, event.source), + label: getLabelText(type, event.source), target: Target(target), operator: event.operator || defaultOperator(event), // value: target ? target.label : event.value, - icon: event.icon || getEventIcon({...event, type: viewType }) + icon: event.icon || getEventIcon({...event, type: type }) }; } }) diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index 42637a2fa..41c3421f8 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -9,7 +9,8 @@ import { getDateRangeFromValue } from 'App/dateRange'; import Event from './event'; -import CustomFilter from './customFilter'; +// import CustomFilter from './customFilter'; +import NewFilter from './newFilter'; const rangeValue = DATE_RANGE_VALUES.LAST_7_DAYS; const range = getDateRangeFromValue(rangeValue); @@ -17,8 +18,8 @@ const startDate = range.start.unix() * 1000; const endDate = range.end.unix() * 1000; export default Record({ - name: undefined, - id: undefined, + name: '', + searchId: undefined, referrer: undefined, userBrowser: undefined, userOs: undefined, @@ -33,6 +34,7 @@ export default Record({ rangeValue, startDate, endDate, + condition: 'then', sort: undefined, order: undefined, @@ -45,6 +47,19 @@ export default Record({ consoleLevel: undefined, strict: false, }, { + idKey: 'searchId', + methods: { + toData() { + const js = this.toJS(); + js.filters = js.filters.map(filter => { + delete filter.operatorOptions + return filter; + }); + + delete js.createdAt; + return js; + } + }, fromJS({ filters, events, custom, ...filter }) { let startDate; let endDate; @@ -59,7 +74,7 @@ export default Record({ startDate, endDate, events: List(events).map(Event), - filters: List(filters).map(CustomFilter), + filters: List(filters).map(NewFilter), custom: Map(custom), } } @@ -73,6 +88,12 @@ export const defaultFilters = [ type: 'default', keys: [ { label: 'Click', key: KEYS.CLICK, type: KEYS.CLICK, filterKey: KEYS.CLICK, icon: 'filters/click', isFilter: false }, + { label: 'DOM Complete', key: KEYS.DOM_COMPLETE, type: KEYS.DOM_COMPLETE, filterKey: KEYS.DOM_COMPLETE, icon: 'filters/click', isFilter: false }, + { label: 'Largest Contentful Paint Time', key: KEYS.LARGEST_CONTENTFUL_PAINT_TIME, type: KEYS.LARGEST_CONTENTFUL_PAINT_TIME, filterKey: KEYS.LARGEST_CONTENTFUL_PAINT_TIME, icon: 'filters/click', isFilter: false }, + { label: 'Time Between Events', key: KEYS.TIME_BETWEEN_EVENTS, type: KEYS.TIME_BETWEEN_EVENTS, filterKey: KEYS.TIME_BETWEEN_EVENTS, icon: 'filters/click', isFilter: false }, + { label: 'Avg CPU Load', key: KEYS.AVG_CPU_LOAD, type: KEYS.AVG_CPU_LOAD, filterKey: KEYS.AVG_CPU_LOAD, icon: 'filters/click', isFilter: false }, + { label: 'Memory Usage', key: KEYS.AVG_MEMORY_USAGE, type: KEYS.AVG_MEMORY_USAGE, filterKey: KEYS.AVG_MEMORY_USAGE, icon: 'filters/click', isFilter: false }, + { label: 'Input', key: KEYS.INPUT, type: KEYS.INPUT, filterKey: KEYS.INPUT, icon: 'event/input', isFilter: false }, { label: 'Page', key: KEYS.LOCATION, type: KEYS.LOCATION, filterKey: KEYS.LOCATION, icon: 'event/link', isFilter: false }, // { label: 'View', key: KEYS.VIEW, type: KEYS.VIEW, filterKey: KEYS.VIEW, icon: 'event/view', isFilter: false } @@ -103,6 +124,11 @@ export const defaultFilters = [ type: 'default', keys: [ { label: 'Errors', key: KEYS.ERROR, type: KEYS.ERROR, filterKey: KEYS.ERROR, icon: 'exclamation-circle', isFilter: false }, + { label: 'Issues', key: KEYS.ISSUES, type: KEYS.ISSUES, filterKey: KEYS.ISSUES, icon: 'exclamation-circle', isFilter: true }, + { label: 'UTM Source', key: KEYS.UTM_SOURCE, type: KEYS.UTM_SOURCE, filterKey: KEYS.UTM_SOURCE, icon: 'exclamation-circle', isFilter: true }, + { label: 'UTM Medium', key: KEYS.UTM_MEDIUM, type: KEYS.UTM_MEDIUM, filterKey: KEYS.UTM_MEDIUM, icon: 'exclamation-circle', isFilter: true }, + { label: 'UTM Campaign', key: KEYS.UTM_CAMPAIGN, type: KEYS.UTM_CAMPAIGN, filterKey: KEYS.UTM_CAMPAIGN, icon: 'exclamation-circle', isFilter: true }, + { label: 'Fetch Requests', key: KEYS.FETCH, type: KEYS.FETCH, filterKey: KEYS.FETCH, icon: 'fetch', isFilter: false }, { label: 'GraphQL Queries', key: KEYS.GRAPHQL, type: KEYS.GRAPHQL, filterKey: KEYS.GRAPHQL, icon: 'vendors/graphql', isFilter: false }, { label: 'Store Actions', key: KEYS.STATEACTION, type: KEYS.STATEACTION, filterKey: KEYS.STATEACTION, icon: 'store', isFilter: false }, @@ -127,6 +153,13 @@ export const getEventIcon = (filter) => { if (type === KEYS.USERBROWSER) return 'window'; if (type === KEYS.PLATFORM) return 'window'; + if (type === TYPES.DOM_COMPLETE) return 'filters/click'; + if (type === TYPES.LARGEST_CONTENTFUL_PAINT_TIME) return 'filters/click'; + if (type === TYPES.TIME_BETWEEN_EVENTS) return 'filters/click'; + if (type === TYPES.TTFB) return 'filters/click'; + if (type === TYPES.AVG_CPU_LOAD) return 'filters/click'; + if (type === TYPES.AVG_MEMORY_USAGE) return 'filters/click'; + if (type === TYPES.CLICK) return 'filters/click'; if (type === TYPES.LOCATION) return 'map-marker-alt'; if (type === TYPES.VIEW) return 'event/view'; diff --git a/frontend/app/types/filter/index.js b/frontend/app/types/filter/index.js index dba5512db..957e1dfb8 100644 --- a/frontend/app/types/filter/index.js +++ b/frontend/app/types/filter/index.js @@ -6,7 +6,7 @@ export * from './filter'; const filterKeys = ['is', 'isNot']; -const stringFilterKeys = ['is', 'isNot', 'contains']; +const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith']; const targetFilterKeys = ['on', 'notOn']; const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp']; const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast']; @@ -209,6 +209,12 @@ export const operatorOptions = (filter) => { case TYPES.REVID: case 'metadata': case KEYS.ERROR: + case TYPES.DOM_COMPLETE: + case TYPES.LARGEST_CONTENTFUL_PAINT_TIME: + case TYPES.TIME_BETWEEN_EVENTS: + case TYPES.TTFB: + case TYPES.AVG_CPU_LOAD: + case TYPES.AVG_MEMORY_USAGE: return stringFilterOptions; case TYPES.INPUT: @@ -230,4 +236,22 @@ export const operatorOptions = (filter) => { case KEYS.CLICK_RAGE: return [{ key: 'onAnything', text: 'on anything', value: 'true' }] } -} \ No newline at end of file +} + +const NewFilterType = (key, category, label, icon, isEvent = false) => { + return { + key: key, + category: category, + label: label, + icon: icon, + isEvent: isEvent, + operators: operatorOptions({ key }), + value: [""] + } +} + +export const newFiltersList = [ + NewFilterType(TYPES.CLICK, 'Gear', 'Click', 'filters/click', true), + NewFilterType(TYPES.CLICK, 'Gear', 'Input', 'filters/click', true), + NewFilterType(TYPES.CONSOLE, 'Other', 'Console', 'filters/click', true), +]; \ No newline at end of file diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js new file mode 100644 index 000000000..2c1a49f48 --- /dev/null +++ b/frontend/app/types/filter/newFilter.js @@ -0,0 +1,308 @@ +import Record from 'Types/Record'; + +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'; + +const ISSUE = 'ISSUE'; +const EVENTS_COUNT = 'EVENTS_COUNT'; +const UTM_SOURCE = 'UTM_SOURCE'; +const UTM_MEDIUM = 'UTM_MEDIUM'; +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 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, +}; + +const filterKeys = ['is', 'isNot']; +const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith']; +const targetFilterKeys = ['on', 'notOn']; +const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp']; +const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast']; + +const options = [ + { + key: 'is', + text: 'is', + value: 'is' + }, { + key: 'isNot', + text: 'is not', + value: 'isNot' + }, { + key: 'startsWith', + text: 'starts with', + value: 'startsWith' + }, { + key: 'endsWith', + text: 'ends with', + value: 'endsWith' + }, { + key: 'contains', + text: 'contains', + value: 'contains' + }, { + key: 'doesNotContain', + text: 'does not contain', + value: 'doesNotContain' + }, { + key: 'hasAnyValue', + text: 'has any value', + value: 'hasAnyValue' + }, { + key: 'hasNoValue', + text: 'has no value', + value: 'hasNoValue' + }, + + + { + key: 'isSignedUp', + text: 'is signed up', + value: 'isSignedUp' + }, { + key: 'notSignedUp', + text: 'not signed up', + value: 'notSignedUp' + }, + + + { + key: 'before', + text: 'before', + value: 'before' + }, { + key: 'after', + text: 'after', + value: 'after' + }, { + key: 'on', + text: 'on', + value: 'on' + }, { + key: 'notOn', + text: 'not on', + value: 'notOn' + }, { + key: 'inRage', + text: 'in rage', + value: 'inRage' + }, { + key: 'notInRage', + text: 'not in rage', + value: 'notInRage' + }, { + key: 'withinLast', + text: 'within last', + value: 'withinLast' + }, { + key: 'notWithinLast', + text: 'not within last', + value: 'notWithinLast' + }, + + { + key: 'greaterThan', + text: 'greater than', + value: 'greaterThan' + }, { + key: 'lessThan', + text: 'less than', + value: 'lessThan' + }, { + key: 'equal', + text: 'equal', + value: 'equal' + }, { + key: 'not equal', + text: 'not equal', + value: 'not equal' + }, + + + { + key: 'onSelector', + text: 'on selector', + value: 'onSelector' + }, { + key: 'onText', + text: 'on text', + value: 'onText' + }, { + key: 'onComponent', + text: 'on component', + value: 'onComponent' + }, + + + { + key: 'onAnything', + text: 'on anything', + value: 'onAnything' + } +]; + +export const filterOptions = options.filter(({key}) => filterKeys.includes(key)); +export const stringFilterOptions = options.filter(({key}) => stringFilterKeys.includes(key)); +export const targetFilterOptions = options.filter(({key}) => targetFilterKeys.includes(key)); +export const booleanOptions = [ + { key: 'true', text: 'true', value: 'true' }, + { key: 'false', text: 'false', value: 'false' }, +] + +export const filtersMap = { + [TYPES.CLICK]: { category: 'interactions', label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click' }, + [TYPES.INPUT]: { category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.LOCATION]: { category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + + [TYPES.USER_OS]: { category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USER_BROWSER]: { category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USER_DEVICE]: { category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.PLATFORM]: { category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.REVID]: { category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + + [TYPES.REFERRER]: { category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.DURATION]: { category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USER_COUNTRY]: { category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + + [TYPES.CONSOLE]: { category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.ERROR]: { category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.FETCH]: { category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.GRAPHQL]: { category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.STATEACTION]: { category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + + [TYPES.USERID]: { category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USERANONYMOUSID]: { category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + + [TYPES.DOM_COMPLETE]: { category: 'new', label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.LARGEST_CONTENTFUL_PAINT_TIME]: { category: 'new', label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.TIME_BETWEEN_EVENTS]: { category: 'new', label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.TTFB]: { category: 'new', label: 'TTFB', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.AVG_CPU_LOAD]: { category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.AVG_MEMORY_USAGE]: { category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.SLOW_SESSION]: { category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' }, + [TYPES.MISSING_RESOURCE]: { category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' }, + [TYPES.CLICK_RAGE]: { category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' }, + // [TYPES.URL]: { category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions }, + // [TYPES.CUSTOM]: { category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions }, + // [TYPES.METADATA]: { category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions }, +} + +export default Record({ + timestamp: 0, + key: '', + label: '', + icon: '', + type: '', + value: [""], + + custom: '', + // target: Target(), + level: '', + source: null, + hasNoValue: false, + isFilter: false, + actualValue: '', + + operator: 'is', + operatorOptions: [], +}, { + keyKey: "_key", + fromJS: ({ ...filter }) => ({ + ...filter, + 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), + // value: target ? target.label : filter.value, + // value: typeof value === 'string' ? [value] : value, + // icon: filter.type ? getfilterIcon(filter.type) : 'filters/metadata' + }), +}) + +// const NewFilterType = (key, category, icon, isEvent = false) => { +// return { +// key: key, +// category: category, +// label: filterMap[key].label, +// icon: icon, +// isEvent: isEvent, +// operators: filterMap[key].operatorOptions, +// value: [""] +// } +// } + +// export const newFiltersList = [ +// NewFilterType(TYPES.CLICK, 'Click', 'filters/click', true), +// NewFilterType(TYPES.CLICK, 'Input', 'filters/click', true), +// NewFilterType(TYPES.CONSOLE, 'Console', 'filters/click', true), +// ]; \ No newline at end of file diff --git a/frontend/env.js b/frontend/env.js index f7c49c874..ad23c710a 100644 --- a/frontend/env.js +++ b/frontend/env.js @@ -5,13 +5,13 @@ require('dotenv').config() const oss = { name: 'oss', - PRODUCTION: true, + PRODUCTION: false, SENTRY_ENABLED: false, SENTRY_URL: "", CAPTCHA_ENABLED: process.env.CAPTCHA_ENABLED === 'true', CAPTCHA_SITE_KEY: process.env.CAPTCHA_SITE_KEY, ORIGIN: () => 'window.location.origin', - API_EDP: () => 'window.location.origin + "/api"', + API_EDP: "https://dol.openreplay.com/api", ASSETS_HOST: () => 'window.location.origin + "/assets"', VERSION: '1.3.6', SOURCEMAP: true, From e80293efa4edbce29cbf5d28483ce07ffb3087b3 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Wed, 19 Jan 2022 14:49:21 +0530 Subject: [PATCH 2/6] feat(ui) - custom metrics --- .../BugFinder/EventFilter/EventFilter.js | 30 ++++---- .../app/components/Dashboard/Dashboard.js | 8 ++ .../CustomMetricWidget/CustomMetricWidget.tsx | 75 +++++++++++++++++++ .../CustomMetricWidget/index.ts | 1 + .../CustomMetricsWidgets.tsx | 29 +++++++ .../Widgets/CustomMetricsWidgets/index.ts | 1 + .../CustomMetricWidgetHoc.css | 6 ++ .../CustomMetricWidgetHoc.tsx | 25 +++++++ .../common/CustomMetricWidgetHoc/index.ts | 1 + .../CustomMetricForm/CustomMetricForm.tsx | 16 ++-- .../shared/CustomMetrics/CustomMetrics.tsx | 29 +++---- .../FilterSeries/FilterSeries.tsx | 75 ++++++++++++------- .../FilterAutoComplete/FilterAutoComplete.css | 30 ++++++-- .../FilterAutoComplete/FilterAutoComplete.tsx | 10 ++- .../shared/Filters/FilterItem/FilterItem.tsx | 13 ++-- .../shared/Filters/FilterList/FilterList.tsx | 32 ++++++++ .../Filters/FilterModal/FilterModal.tsx | 4 +- .../FilterSelection/FilterSelection.tsx | 11 +-- .../Filters/FilterValue/FilterValue.tsx | 10 ++- .../ui/SegmentSelection/segmentSelection.css | 8 +- frontend/app/duck/customMetrics.js | 13 +--- frontend/app/duck/filters.js | 4 +- frontend/app/styles/main.css | 3 +- frontend/app/types/customMetric.js | 4 + frontend/app/types/filter/filter.js | 3 + frontend/app/types/filter/newFilter.js | 72 +++++++++--------- 26 files changed, 365 insertions(+), 148 deletions(-) create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.tsx create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/index.ts create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricsWidgets.tsx create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/index.ts create mode 100644 frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.css create mode 100644 frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.tsx create mode 100644 frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/index.ts diff --git a/frontend/app/components/BugFinder/EventFilter/EventFilter.js b/frontend/app/components/BugFinder/EventFilter/EventFilter.js index 21467c382..5bc1d1b32 100644 --- a/frontend/app/components/BugFinder/EventFilter/EventFilter.js +++ b/frontend/app/components/BugFinder/EventFilter/EventFilter.js @@ -141,21 +141,21 @@ export default class EventFilter extends React.PureComponent { { hasFilters &&
-
Operator
- -
+
Operator
+ +
{ events.size > 0 && <> diff --git a/frontend/app/components/Dashboard/Dashboard.js b/frontend/app/components/Dashboard/Dashboard.js index 6af004136..81d7a1de9 100644 --- a/frontend/app/components/Dashboard/Dashboard.js +++ b/frontend/app/components/Dashboard/Dashboard.js @@ -39,6 +39,7 @@ import SideMenuSection from './SideMenu/SideMenuSection'; import styles from './dashboard.css'; import WidgetSection from 'Shared/WidgetSection/WidgetSection'; import OverviewWidgets from './Widgets/OverviewWidgets/OverviewWidgets'; +import CustomMetricsWidgets from './Widgets/CustomMetricsWidgets/CustomMetricsWidgets'; import WidgetHolder from './WidgetHolder/WidgetHolder'; import MetricsFilters from 'Shared/MetricsFilters/MetricsFilters'; import { withRouter } from 'react-router'; @@ -47,6 +48,7 @@ const OVERVIEW = 'overview'; const PERFORMANCE = 'performance'; const ERRORS_N_CRASHES = 'errors_n_crashes'; const RESOURCES = 'resources'; +const CUSTOM_METRICS = 'custom_metrics'; const menuList = [ { @@ -201,6 +203,12 @@ export default class Dashboard extends React.PureComponent {
+ +
+ +
+
+
{ dashboardAppearance.impactedSessionsByJsErrors && } diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.tsx new file mode 100644 index 000000000..9de11f57e --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Loader, NoContent } from 'UI'; +import { widgetHOC, Styles } from '../../common'; +import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts'; +import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period'; +import CustomMetricWidgetHoc from '../../common/CustomMetricWidgetHoc'; + +const customParams = rangeName => { + const params = { density: 70 } + + if (rangeName === LAST_24_HOURS) params.density = 70 + if (rangeName === LAST_30_MINUTES) params.density = 70 + if (rangeName === YESTERDAY) params.density = 70 + if (rangeName === LAST_7_DAYS) params.density = 70 + + return params +} + +interface Period { + rangeName: string; +} + +interface Props { + widget: any; + loading?: boolean; + data?: any; + showSync?: boolean; + compare?: boolean; + period?: Period; +} +function CustomMetricWidget(props: Props) { + const { widget, loading = false, data = { chart: []}, showSync, compare, period = { rangeName: ''} } = props; + + const colors = compare ? Styles.compareColors : Styles.colors; + const params = customParams(period.rangeName) + const gradientDef = Styles.gradientDef(); + return ( + + + + + {gradientDef} + + + + + + + + + + ); +} + +export default CustomMetricWidgetHoc(CustomMetricWidget); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/index.ts new file mode 100644 index 000000000..4a6d9b653 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/index.ts @@ -0,0 +1 @@ +export { default } from './CustomMetricWidget'; \ 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 new file mode 100644 index 000000000..3ea473f61 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricsWidgets.tsx @@ -0,0 +1,29 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { fetchList } from 'Duck/customMetrics'; +import { list } from 'App/components/BugFinder/CustomFilters/filterModal.css'; +import CustomMetricWidget from './CustomMetricWidget'; + +interface Props { + fetchList: Function; + list: any; +} +function CustomMetricsWidgets(props: Props) { + const { list } = props; + + useEffect(() => { + props.fetchList() + }, []) + + return ( + <> + {list.map((item: any) => ( + + ))} + + ); +} + +export default connect(state => ({ + list: state.getIn(['customMetrics', 'list']), +}), { fetchList })(CustomMetricsWidgets); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/index.ts new file mode 100644 index 000000000..54d9a4192 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/index.ts @@ -0,0 +1 @@ +export { default } from './CustomMetricsWidgets'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.css b/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.css new file mode 100644 index 000000000..1d1ef3ee4 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.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/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.tsx b/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.tsx new file mode 100644 index 000000000..ba4a2726a --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import stl from './CustomMetricWidgetHoc.css'; +import { Icon } from 'UI'; + +interface Props { +} +const CustomMetricWidgetHoc = ({ ...rest }: Props) => BaseComponent => { + + console.log('CustomMetricWidgetHoc', rest); + return ( +
+
+
Widget Name
+
+
+ +
+
+
+ {/* */} +
+ ); +} + +export default CustomMetricWidgetHoc; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/index.ts b/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/index.ts new file mode 100644 index 000000000..0be8a5be5 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/index.ts @@ -0,0 +1 @@ +export { default } from './CustomMetricWidgetHoc'; \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx index 20337cbc7..243a51ced 100644 --- a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx +++ b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Form, SegmentSelection, Button } from 'UI'; +import { Form, SegmentSelection, Button, IconButton } from 'UI'; import FilterSeries from '../FilterSeries'; import { connect } from 'react-redux'; import { edit as editMetric, save } from 'Duck/customMetrics'; @@ -68,13 +68,13 @@ function CustomMetricForm(props: Props) {
- Timeseries - of -
+ Timeseries + of +
- + {metric.series && metric.series.size > 0 && metric.series.map((series: any, index: number) => (
- +
+ +
diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx index ae7102202..c11682513 100644 --- a/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx +++ b/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx @@ -1,38 +1,29 @@ -import React from 'react'; +import React, { useState } from 'react'; import { IconButton, SlideModal } from 'UI' import CustomMetricForm from './CustomMetricForm'; interface Props {} function CustomMetrics(props: Props) { + const [showModal, setShowModal] = useState(true); + return ( -
- +
+ setShowModal(true)} /> { 'Custom Metric' } - {/* toggleForm({}, true) } - /> */}
} - isDisplayed={ true } - // onClose={ () => { - // toggleForm({}, false); - // setShowAlerts(false); - // } } + isDisplayed={ showModal } + onClose={ () => setShowModal(false)} // size="medium" - content={ -
+ content={ showModal && ( +
- } + )} />
); diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx b/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx index f29301644..69a1637d5 100644 --- a/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import FilterList from 'Shared/Filters/FilterList'; import { edit, updateSeries } from 'Duck/customMetrics'; import { connect } from 'react-redux'; -import { IconButton, Button, Icon } from 'UI'; +import { IconButton, Button, Icon, SegmentSelection } from 'UI'; import FilterSelection from '../../Filters/FilterSelection'; interface Props { @@ -14,6 +14,7 @@ interface Props { } function FilterSeries(props: Props) { + const [expanded, setExpanded] = useState(false) const { series, seriesIndex } = props; const onAddFilter = (filter) => { @@ -46,6 +47,16 @@ function FilterSeries(props: Props) { }); } + const onChangeEventsOrder = (e, { name, value }) => { + props.updateSeries(seriesIndex, { + ...series.toData(), + filter: { + ...series.filter, + eventsOrder: value, + } + }); + } + const onRemoveFilter = (filterIndex) => { const newFilters = series.filter.filters.filter((_filter, i) => { return i !== filterIndex; @@ -62,35 +73,43 @@ function FilterSeries(props: Props) { return (
-
- { series.name } -
- +
+
{ series.name }
+ +
+
+ +
+ +
setExpanded(!expanded)} className="ml-3"> + +
+
-
- { series.filter.filters.size > 0 ? ( - - ): ( -
Add user event or filter to build the series.
- )} -
-
- - {/* */} - - -
+ { expanded && ( + <> +
+ { series.filter.filters.size > 0 ? ( + + ): ( +
Add user event or filter to build the series.
+ )} +
+
+ + + +
+ + )}
); } diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.css b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.css index ba7359239..e2ce40ac0 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.css +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.css @@ -16,20 +16,36 @@ & .right { height: 28px; display: flex; - align-items: center; - padding: 0 5px; + align-items: stretch; + padding: 0; background-color: $gray-lightest; - border-left: solid thin $gray-light !important; border-top-right-radius: 3px; border-bottom-right-radius: 3px; - cursor: pointer; + + & div { + /* background-color: red; */ + border-left: solid thin $gray-light !important; + width: 28px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + &:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } + &:hover { + background-color: $gray-light; + } + } } } .menu { border-radius: 0 0 3px 3px; - box-shadow: 0 2px 10px 0 $gray-light; - padding: 20px; + border: solid thin $gray-light !important; + /* box-shadow: 0 2px 10px 0 $gray-light; */ + /* padding: 20px; */ background-color: white; max-height: 350px; overflow-y: auto; @@ -43,7 +59,7 @@ .filterItem { display: flex; align-items: center; - padding: 8px; + padding: 8px 10px; cursor: pointer; border-radius: 3px; transition: all 0.4s; diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index 068b255f7..fd215c11e 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -12,6 +12,7 @@ const hiddenStyle = { interface Props { showOrButton?: boolean; + showCloseButton?: boolean; onRemoveValue?: () => void; onAddValue?: () => void; endpoint?: string; @@ -25,6 +26,7 @@ interface Props { function FilterAutoComplete(props: Props) { const { + showCloseButton = false, placeholder = 'Type to search', method = 'GET', showOrButton = false, @@ -94,7 +96,7 @@ function FilterAutoComplete(props: Props) { } return ( -
+
- { !showOrButton && } - { showOrButton && or} + { showCloseButton &&
} + { showOrButton &&
or
}
+ { !showOrButton &&
or
} + {/* */} { showModal && (options.length > 0 || loading) && diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index 66aa08ff3..c1e35f250 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -44,26 +44,29 @@ function FitlerItem(props: Props) { } return ( -
+
{filterIndex+1}
- +
{filter.value && filter.value.map((value, valueIndex) => ( 1} showOrButton={valueIndex === filter.value.length - 1} - key={valueIndex} + // filter={filter} + // key={valueIndex} value={value} + key={filter.key} index={valueIndex} onAddValue={onAddValue} onRemoveValue={() => onRemoveValue(valueIndex)} - onSelect={(e, item) => onSelect(e, valueIndex, item)} + onSelect={(e, item) => onSelect(e, item, valueIndex)} /> ))}
-
+
+
+
EVENTS
+
+
Events Order
+ null } + // value={{ value: series.filter.eventsOrder }} + value={{ value: 'and' }} + list={ [ + { name: 'AND', value: 'and' }, + { name: 'OR', value: 'or' }, + { name: 'THEN', value: 'then' }, + ]} + /> +
+
{filters.map((filter, filterIndex) => ( onRemoveFilter(filterIndex) } /> ))} + + {/*
Filters
+ {filters.filter(f => !f.isEvent).map((filter, filterIndex) => ( + props.onUpdateFilter(filterIndex, filter)} + onRemoveFilter={() => onRemoveFilter(filterIndex) } + /> + ))} */}
); } diff --git a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx index 4a16b6511..b2299b2e2 100644 --- a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx +++ b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx @@ -9,14 +9,14 @@ interface Props { function FilterModal(props: Props) { const { filters, onFilterClick = () => null } = props; return ( -
+
{filters && Object.keys(filters).map((key) => (
{key}
{filters[key].map((filter: any) => ( -
onFilterClick(filter)}> +
onFilterClick(filter)}> {filter.label}
diff --git a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx index e899f6566..17cc7d288 100644 --- a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx +++ b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx @@ -10,19 +10,20 @@ interface Props { } function FilterSelection(props: Props) { const { filter, onFilterClick, children } = props; - const [showModal, setShowModal] = useState(false) + const [showModal, setShowModal] = useState(false); + return ( -
+
setTimeout(function() { setShowModal(false) - }, 20)} + }, 50)} > { children ? React.cloneElement(children, { onClick: () => setShowModal(true)}) : (
setShowModal(true)} > {filter.label} diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 65e123ba4..3483b3340 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -4,25 +4,29 @@ import FilterAutoComplete from '../FilterAutoComplete'; interface Props { index: number; value: any; // event/filter + // type: string; + key: string; onRemoveValue?: () => void; onAddValue?: () => void; + showCloseButton: boolean; showOrButton: boolean; onSelect: (e, item) => void; } function FilterValue(props: Props) { - const { index, value, showOrButton, onRemoveValue , onAddValue } = props; + const { index, value, key, showOrButton, showCloseButton, onRemoveValue , onAddValue } = props; return ( ); diff --git a/frontend/app/components/ui/SegmentSelection/segmentSelection.css b/frontend/app/components/ui/SegmentSelection/segmentSelection.css index a74d7d77b..e33ec3b73 100644 --- a/frontend/app/components/ui/SegmentSelection/segmentSelection.css +++ b/frontend/app/components/ui/SegmentSelection/segmentSelection.css @@ -3,16 +3,16 @@ align-items: center; justify-content: space-around; border: solid thin $gray-light; - border-radius: 5px; + border-radius: 3px; overflow: hidden; & .item { color: $gray-medium; font-weight: medium; padding: 10px; - flex: 1; + /* flex: 1; */ text-align: center; - border-right: solid thin $gray-light; + border-right: solid thin $teal; cursor: pointer; background-color: $gray-lightest; display: flex; @@ -64,6 +64,6 @@ } .extraSmall .item { - padding: 2px 4px; + padding: 0 4px; font-size: 12px; } \ No newline at end of file diff --git a/frontend/app/duck/customMetrics.js b/frontend/app/duck/customMetrics.js index 0909ff3b1..f6a7b0b88 100644 --- a/frontend/app/duck/customMetrics.js +++ b/frontend/app/duck/customMetrics.js @@ -48,6 +48,7 @@ function reducer(state = initialState, action = {}) { case EDIT: return state.mergeIn([ 'instance' ], CustomMetric(action.instance)); case UPDATE_SERIES: + console.log('update series', action.series); return state.setIn(['instance', 'series', action.index], FilterSeries(action.series)); case success(SAVE): return state.set([ 'instance' ], CustomMetric(action.data)); @@ -55,11 +56,7 @@ function reducer(state = initialState, action = {}) { return state.set("instance", ErrorInfo(action.data)); case success(FETCH_LIST): const { data } = action; - return state - .set("totalCount", data ? data.total : 0) - .set("list", List(data && data.errors).map(CustomMetric) - .filter(e => e.parentErrorId == null) - .map(e => e.update("chart", chartWrapper))); + return state.set("list", List(data.map(CustomMetric))); } return state; } @@ -95,11 +92,9 @@ export function save(instance) { }; } -export function fetchList(params = {}, clear = false) { +export function fetchList() { return { types: array(FETCH_LIST), - call: client => client.post('/errors/search', params), - clear, - params: cleanParams(params), + call: client => client.get(`/${name}s`), }; } \ No newline at end of file diff --git a/frontend/app/duck/filters.js b/frontend/app/duck/filters.js index 290a4f1cb..9122fb9c2 100644 --- a/frontend/app/duck/filters.js +++ b/frontend/app/duck/filters.js @@ -14,11 +14,9 @@ import { newFiltersList } from 'Types/filter' import NewFilter, { filtersMap } from 'Types/filter/newFilter'; const filterOptions = {} -// newFiltersList.forEach(filter => { -// filterOptions[filter.category] = filter -// }) Object.keys(filtersMap).forEach(key => { + // const filter = NewFilter(filtersMap[key]); const filter = filtersMap[key]; if (filterOptions.hasOwnProperty(filter.category)) { filterOptions[filter.category].push(filter); diff --git a/frontend/app/styles/main.css b/frontend/app/styles/main.css index 077ec9904..8915fd341 100644 --- a/frontend/app/styles/main.css +++ b/frontend/app/styles/main.css @@ -107,8 +107,9 @@ } .form-group { - margin-bottom: 20px; + margin-bottom: 25px; & label { + display: inline-block; margin-bottom: 5px; } } \ No newline at end of file diff --git a/frontend/app/types/customMetric.js b/frontend/app/types/customMetric.js index 32c4386b6..1bfaebbd6 100644 --- a/frontend/app/types/customMetric.js +++ b/frontend/app/types/customMetric.js @@ -17,6 +17,7 @@ export const FilterSeries = Record({ methods: { toData() { const js = this.toJS(); + delete js.key; // js.filter = js.filter.toData(); return js; }, @@ -42,6 +43,7 @@ export default Record({ toData() { const js = this.toJS(); + js.series = js.series.map(series => { series.filter.filters = series.filter.filters.map(filter => { delete filter.operatorOptions @@ -51,6 +53,8 @@ export default Record({ return series; }); + delete js.key; + return js; }, }, diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index 41c3421f8..e3663e860 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -46,6 +46,7 @@ export default Record({ suspicious: undefined, consoleLevel: undefined, strict: false, + eventsOrder: 'and', }, { idKey: 'searchId', methods: { @@ -53,10 +54,12 @@ export default Record({ const js = this.toJS(); js.filters = js.filters.map(filter => { delete filter.operatorOptions + delete filter._key return filter; }); delete js.createdAt; + delete js.key; return js; } }, diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 2c1a49f48..091b16277 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -219,41 +219,41 @@ export const booleanOptions = [ ] export const filtersMap = { - [TYPES.CLICK]: { category: 'interactions', label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click' }, - [TYPES.INPUT]: { category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.LOCATION]: { category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.CLICK]: { key: TYPES.CLICK, type: 'multiple', category: 'interactions', label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click', isEvent: true }, + [TYPES.INPUT]: { key: TYPES.INPUT, type: 'multiple', category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true }, + [TYPES.LOCATION]: { key: TYPES.LOCATION, type: 'multiple', category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true }, - [TYPES.USER_OS]: { category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.USER_BROWSER]: { category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.USER_DEVICE]: { category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.PLATFORM]: { category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.REVID]: { category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USER_OS]: { key: TYPES.USER_OS, type: 'multiple', category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USER_BROWSER]: { key: TYPES.USER_BROWSER, type: 'multiple', category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USER_DEVICE]: { key: TYPES.USER_DEVICE, type: 'multiple', category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.PLATFORM]: { key: TYPES.PLATFORM, type: 'multiple', category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.REVID]: { key: TYPES.REVID, type: 'multiple', category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.REFERRER]: { category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.DURATION]: { category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.USER_COUNTRY]: { category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.REFERRER]: { key: TYPES.REFERRER, type: 'multiple', category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.DURATION]: { key: TYPES.DURATION, type: 'number', category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USER_COUNTRY]: { key: TYPES.USER_COUNTRY, type: 'multiple', category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.CONSOLE]: { category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.ERROR]: { category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.FETCH]: { category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.GRAPHQL]: { category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.STATEACTION]: { category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.CONSOLE]: { key: TYPES.CONSOLE, type: 'multiple', category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.ERROR]: { key: TYPES.ERROR, type: 'multiple', category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.FETCH]: { key: TYPES.FETCH, type: 'multiple', category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.GRAPHQL]: { key: TYPES.GRAPHQL, type: 'multiple', category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.STATEACTION]: { key: TYPES.STATEACTION, type: 'multiple', category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.USERID]: { category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.USERANONYMOUSID]: { category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USERID]: { key: TYPES.USERID, type: 'multiple', category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.USERANONYMOUSID]: { key: TYPES.USERANONYMOUSID, type: 'multiple', category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.DOM_COMPLETE]: { category: 'new', label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.LARGEST_CONTENTFUL_PAINT_TIME]: { category: 'new', label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.TIME_BETWEEN_EVENTS]: { category: 'new', label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.TTFB]: { category: 'new', label: 'TTFB', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.AVG_CPU_LOAD]: { category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.AVG_MEMORY_USAGE]: { category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, - [TYPES.SLOW_SESSION]: { category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' }, - [TYPES.MISSING_RESOURCE]: { category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' }, - [TYPES.CLICK_RAGE]: { category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' }, - // [TYPES.URL]: { category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions }, - // [TYPES.CUSTOM]: { category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions }, - // [TYPES.METADATA]: { category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions }, + [TYPES.DOM_COMPLETE]: { key: TYPES.DOM_COMPLETE, type: 'multiple', category: 'new', label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.LARGEST_CONTENTFUL_PAINT_TIME]: { key: TYPES.LARGEST_CONTENTFUL_PAINT_TIME, type: 'number', category: 'new', label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.TIME_BETWEEN_EVENTS]: { key: TYPES.TIME_BETWEEN_EVENTS, type: 'number', category: 'new', label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.TTFB]: { key: TYPES.TTFB, type: 'time', category: 'new', label: 'TTFB', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.AVG_CPU_LOAD]: { key: TYPES.AVG_CPU_LOAD, type: 'number', category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.AVG_MEMORY_USAGE]: { key: TYPES.AVG_MEMORY_USAGE, type: 'number', category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [TYPES.SLOW_SESSION]: { key: TYPES.SLOW_SESSION, type: 'boolean', category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' }, + [TYPES.MISSING_RESOURCE]: { key: TYPES.MISSING_RESOURCE, type: 'boolean', category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' }, + [TYPES.CLICK_RAGE]: { key: TYPES.CLICK_RAGE, type: 'boolean', category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' }, + // [TYPES.URL]: { / [TYPES,TYPES. category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions }, + // [TYPES.CUSTOM]: { / [TYPES,TYPES. category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions }, + // [TYPES.METADATA]: { / [TYPES,TYPES. category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions }, } export default Record({ @@ -263,6 +263,7 @@ export default Record({ icon: '', type: '', value: [""], + category: '', custom: '', // target: Target(), @@ -274,10 +275,13 @@ export default Record({ operator: 'is', operatorOptions: [], + isEvent: false, + index: 0, }, { keyKey: "_key", fromJS: ({ ...filter }) => ({ ...filter, + key: filter.type, type: filter.type, // camelCased(filter.type.toLowerCase()), // key: filter.type === METADATA ? filter.label : filter.key || filter.type, // || camelCased(filter.type.toLowerCase()), // label: getLabel(filter), @@ -299,10 +303,4 @@ export default Record({ // operators: filterMap[key].operatorOptions, // value: [""] // } -// } - -// export const newFiltersList = [ -// NewFilterType(TYPES.CLICK, 'Click', 'filters/click', true), -// NewFilterType(TYPES.CLICK, 'Input', 'filters/click', true), -// NewFilterType(TYPES.CONSOLE, 'Console', 'filters/click', true), -// ]; \ No newline at end of file +// } \ No newline at end of file From af260a7530deb4d47ba67f9d10fa9ce2505ec866 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Sun, 23 Jan 2022 23:25:28 +0530 Subject: [PATCH 3/6] feat(ui) - custom metrics --- frontend/app/components/Alerts/AlertForm.js | 18 +- .../Alerts/AlertFormModal/AlertFormModal.tsx | 91 +++++++ .../components/Alerts/AlertFormModal/index.ts | 1 + .../app/components/Dashboard/Dashboard.js | 2 +- .../CustomMetricWidget/CustomMetricWidget.css | 6 + .../CustomMetricWidget/CustomMetricWidget.tsx | 147 ++++++++--- .../CustomMetricWidgetPreview.css | 6 + .../CustomMetricWidgetPreview.tsx | 110 ++++++++ .../CustomMetricWidgetPreview/index.ts | 1 + .../CustomMetricsWidgets.tsx | 17 +- .../CustomMetricForm/CustomMetricForm.tsx | 9 +- .../shared/CustomMetrics/CustomMetrics.tsx | 31 ++- .../FilterSeries/FilterSeries.tsx | 15 +- .../FilterSeries/SeriesName/SeriesName.tsx | 46 ++++ .../FilterSeries/SeriesName/index.ts | 1 + .../FilterAutoComplete/FilterAutoComplete.tsx | 2 +- .../Filters/FilterDuration/FilterDuration.css | 24 ++ .../Filters/FilterDuration/FilterDuration.js | 66 +++++ .../shared/Filters/FilterDuration/index.js | 1 + .../shared/Filters/FilterItem/FilterItem.tsx | 61 ++--- .../Filters/FilterOperator/FilterOperator.tsx | 4 +- .../shared/Filters/FilterOperator/index.ts | 1 + .../Filters/FilterValue/FilterValue.tsx | 104 ++++++-- .../FilterValueDropdown.css | 19 ++ .../FilterValueDropdown.tsx | 31 +++ .../Filters/FilterValueDropdown/index.ts | 1 + .../ui/SegmentSelection/segmentSelection.css | 3 +- frontend/app/duck/alerts.js | 41 ++- frontend/app/duck/customMetrics.js | 22 +- frontend/app/types/customMetric.js | 13 +- frontend/app/types/dashboard/customMetric.js | 14 + frontend/app/types/filter/filter.js | 4 +- frontend/app/types/filter/filterType.ts | 49 ++++ frontend/app/types/filter/newFilter.js | 239 ++++++++++-------- frontend/env.js | 2 +- 35 files changed, 963 insertions(+), 239 deletions(-) create mode 100644 frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx create mode 100644 frontend/app/components/Alerts/AlertFormModal/index.ts create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidget/CustomMetricWidget.css create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.css create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/index.ts create mode 100644 frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/SeriesName.tsx create mode 100644 frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/index.ts create mode 100644 frontend/app/components/shared/Filters/FilterDuration/FilterDuration.css create mode 100644 frontend/app/components/shared/Filters/FilterDuration/FilterDuration.js create mode 100644 frontend/app/components/shared/Filters/FilterDuration/index.js create mode 100644 frontend/app/components/shared/Filters/FilterOperator/index.ts create mode 100644 frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.css create mode 100644 frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx create mode 100644 frontend/app/components/shared/Filters/FilterValueDropdown/index.ts create mode 100644 frontend/app/types/dashboard/customMetric.js create mode 100644 frontend/app/types/filter/filterType.ts 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, From 5f958ede9889a1b3029b988f9e7a9f270f375429 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 24 Jan 2022 19:29:27 +0530 Subject: [PATCH 4/6] feat(ui) - custom metrics --- .../app/components/BugFinder/BugFinder.js | 4 +- .../shared/FilterDropdown/FilterDropdown.js | 2 +- .../shared/Filters/FilterItem/FilterItem.tsx | 21 +-- .../Filters/FilterOperator/FilterOperator.tsx | 2 + .../Filters/FilterValue/FilterValue.tsx | 1 + .../FilterValueDropdown.tsx | 4 +- .../shared/SessionSearch/SessionSearch.tsx | 93 +++++++++++ .../components/shared/SessionSearch/index.ts | 1 + frontend/app/duck/index.js | 2 + frontend/app/duck/search.js | 147 ++++++++++++++++++ frontend/app/svg/icons/filters/browser.svg | 3 + frontend/app/svg/icons/filters/clickrage.svg | 4 + frontend/app/svg/icons/filters/code.svg | 1 + frontend/app/svg/icons/filters/country.svg | 4 + frontend/app/svg/icons/filters/device.svg | 7 + frontend/app/svg/icons/filters/duration.svg | 1 + frontend/app/svg/icons/filters/error.svg | 4 + frontend/app/svg/icons/filters/fetch.svg | 3 + frontend/app/svg/icons/filters/graphql.svg | 1 + frontend/app/svg/icons/filters/i-cursor.svg | 1 + frontend/app/svg/icons/filters/input.svg | 4 + frontend/app/svg/icons/filters/link.svg | 4 + frontend/app/svg/icons/filters/location.svg | 3 + frontend/app/svg/icons/filters/os.svg | 3 + .../{phone-laptop.svg => platform.svg} | 0 frontend/app/svg/icons/filters/referrer.svg | 4 + frontend/app/svg/icons/filters/resize.svg | 10 ++ .../filters/{border-outer.svg => rev-id.svg} | 0 .../app/svg/icons/filters/state-action.svg | 3 + frontend/app/svg/icons/filters/view.svg | 4 + frontend/app/types/filter/filter.js | 9 +- frontend/app/types/filter/filterType.ts | 1 + frontend/app/types/filter/newFilter.js | 98 ++++-------- 33 files changed, 354 insertions(+), 95 deletions(-) create mode 100644 frontend/app/components/shared/SessionSearch/SessionSearch.tsx create mode 100644 frontend/app/components/shared/SessionSearch/index.ts create mode 100644 frontend/app/duck/search.js create mode 100644 frontend/app/svg/icons/filters/browser.svg create mode 100644 frontend/app/svg/icons/filters/clickrage.svg create mode 100644 frontend/app/svg/icons/filters/code.svg create mode 100644 frontend/app/svg/icons/filters/country.svg create mode 100644 frontend/app/svg/icons/filters/device.svg create mode 100644 frontend/app/svg/icons/filters/duration.svg create mode 100644 frontend/app/svg/icons/filters/error.svg create mode 100644 frontend/app/svg/icons/filters/fetch.svg create mode 100644 frontend/app/svg/icons/filters/graphql.svg create mode 100644 frontend/app/svg/icons/filters/i-cursor.svg create mode 100644 frontend/app/svg/icons/filters/input.svg create mode 100644 frontend/app/svg/icons/filters/link.svg create mode 100644 frontend/app/svg/icons/filters/location.svg create mode 100644 frontend/app/svg/icons/filters/os.svg rename frontend/app/svg/icons/filters/{phone-laptop.svg => platform.svg} (100%) create mode 100644 frontend/app/svg/icons/filters/referrer.svg create mode 100644 frontend/app/svg/icons/filters/resize.svg rename frontend/app/svg/icons/filters/{border-outer.svg => rev-id.svg} (100%) create mode 100644 frontend/app/svg/icons/filters/state-action.svg create mode 100644 frontend/app/svg/icons/filters/view.svg diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index 4af7870f2..93ad66347 100644 --- a/frontend/app/components/BugFinder/BugFinder.js +++ b/frontend/app/components/BugFinder/BugFinder.js @@ -29,6 +29,7 @@ import TrackerUpdateMessage from 'Shared/TrackerUpdateMessage'; import SessionSearchField from 'Shared/SessionSearchField' import SavedSearch from 'Shared/SavedSearch' import LiveSessionList from './LiveSessionList' +import SessionSearch from 'Shared/SessionSearch'; const weakEqual = (val1, val2) => { if (!!val1 === false && !!val2 === false) return true; @@ -176,7 +177,8 @@ export default class BugFinder extends React.PureComponent {
- + + {/* */}
{ activeFlow && activeFlow.type === 'flows' && } { activeTab.type !== 'live' && } diff --git a/frontend/app/components/shared/FilterDropdown/FilterDropdown.js b/frontend/app/components/shared/FilterDropdown/FilterDropdown.js index 8bf6e5faa..66da1f586 100644 --- a/frontend/app/components/shared/FilterDropdown/FilterDropdown.js +++ b/frontend/app/components/shared/FilterDropdown/FilterDropdown.js @@ -27,7 +27,7 @@ const locationOptions = Object.keys(regionLabels).map(k => ({ key: LOCATION, tex const _filterKeys = [ { key: 'userId', name: 'User ID', icon: 'user-alt', placeholder: 'Search for User ID' }, { key: 'userAnonymousId', name: 'User Anonymous ID', icon: 'filters/userid', placeholder: 'Search for User Anonymous ID' }, - { key: 'revId', name: 'Rev ID', icon: 'filters/border-outer', placeholder: 'Search for Rev ID' }, + { key: 'revId', name: 'Rev ID', icon: 'filters/rev-id', placeholder: 'Search for Rev ID' }, { key: COUNTRY, name: 'Country', icon: 'map-marker-alt', placeholder: 'Search for Country' }, { key: 'device', name: 'Device', icon: 'device', placeholder: 'Search for Device' }, { key: 'os', name: 'OS', icon: 'os', placeholder: 'Search for OS' }, diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index c45b7620a..2b0231da6 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -37,9 +37,8 @@ function FitlerItem(props: Props) { // onUpdate({ ...filter, value: newValues }) // } - console.log('filter', filter); - const onOperatorChange = (e, { name, value }) => { + console.log('onOperatorChange', name, value) onUpdate({ ...filter, operator: value }) } @@ -49,23 +48,7 @@ function FitlerItem(props: Props) {
{filterIndex+1}
- {/*
*/} - {/* {filter.value && filter.value.map((value, valueIndex) => ( */} - 1} - // showOrButton={valueIndex === filter.value.length - 1} - filter={filter} - onUpdate={onUpdate} - // key={valueIndex} - // value={value} - // key={filter.key} - // index={valueIndex} - // onAddValue={onAddValue} - // onRemoveValue={(valueIndex) => onRemoveValue(valueIndex)} - // onSelect={(e, item, valueIndex) => onSelect(e, item, valueIndex)} - /> - {/* ))} */} - {/*
*/} +
{ switch(filter.type) { case FilterType.ISSUE: + case FilterType.DROPDOWN: return ( void; className?: string; options: any[]; + search?: boolean; } function FilterValueDropdown(props: Props) { - const { options, onChange, value, className = '' } = props; + const { search = false, options, onChange, value, className = '' } = props; // const options = [] return ( { + filter.value = [""] + const newFilters = appliedFilter.filter.filters.concat(filter); + props.edit({ + ...appliedFilter, + filter: { + ...appliedFilter.filter, + filters: newFilters, + } + }); + } + + const onUpdateFilter = (filterIndex, filter) => { + const newFilters = appliedFilter.filter.filters.map((_filter, i) => { + if (i === filterIndex) { + return filter; + } else { + return _filter; + } + }); + + props.edit({ + ...appliedFilter.toData(), + filter: { + ...appliedFilter.filter, + filters: newFilters, + } + }); + } + + const onRemoveFilter = (filterIndex) => { + const newFilters = appliedFilter.filter.filters.filter((_filter, i) => { + return i !== filterIndex; + }); + + props.edit({ + ...appliedFilter, + filter: { + ...appliedFilter.filter, + filters: newFilters, + } + }); + } + + return ( +
+
+ +
+ +
+
+ + + +
+
+ + + +
+
+
+ ); +} + +export default connect(state => ({ + appliedFilter: state.getIn([ 'search', 'instance' ]), +}), { edit })(SessionSearch); + +// appliedFilter: state.getIn([ 'filters', 'appliedFilter' ]), \ No newline at end of file diff --git a/frontend/app/components/shared/SessionSearch/index.ts b/frontend/app/components/shared/SessionSearch/index.ts new file mode 100644 index 000000000..d9c909f0d --- /dev/null +++ b/frontend/app/components/shared/SessionSearch/index.ts @@ -0,0 +1 @@ +export { default } from './SessionSearch'; \ No newline at end of file diff --git a/frontend/app/duck/index.js b/frontend/app/duck/index.js index 71eb6d0e2..ffd0b945d 100644 --- a/frontend/app/duck/index.js +++ b/frontend/app/duck/index.js @@ -35,6 +35,7 @@ import funnels from './funnels'; import config from './config'; import roles from './roles'; import customMetrics from './customMetrics'; +import search from './search'; export default combineReducers({ jwt, @@ -70,6 +71,7 @@ export default combineReducers({ config, roles, customMetrics, + search, ...integrations, ...sources, }); diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js new file mode 100644 index 000000000..fee81b122 --- /dev/null +++ b/frontend/app/duck/search.js @@ -0,0 +1,147 @@ +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, 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'; +import Filter from 'Types/filter'; +// import filter from 'core-js/fn/array/filter'; +// import NewFilter from 'Types/filter/newFilter'; +// import Event from 'Types/filter/event'; +// import CustomFilter from 'Types/filter/customFilter'; +import { errors as errorsRoute, isRoute } from "App/routes"; +import { fetchList as fetchSessionList } from './sessions'; +import { fetchList as fetchErrorsList } from './errors'; + +const ERRORS_ROUTE = errorsRoute(); + +const name = "custom_metric"; +const idKey = "metricId"; + +const FETCH_LIST = fetchListType(name); +const FETCH = fetchType(name); +const SAVE = saveType(name); +const EDIT = editType(name); +const REMOVE = removeType(name); +const UPDATE = `${name}/UPDATE`; +const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`; + +function chartWrapper(chart = []) { + return chart.map(point => ({ ...point, count: Math.max(point.count, 0) })); +} + +// const updateItemInList = createListUpdater(idKey); +// const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ] +// ? state.mergeIn([ "instance" ], instance) +// : state; + +const initialState = Map({ + list: List(), + alertMetricId: null, + // instance: null, + instance: FilterSeries({ filter: new Filter({ filters: [] }) }), +}); + +// Metric - Series - [] - filters +function reducer(state = initialState, action = {}) { + switch (action.type) { + case EDIT: + return state.set('instance', FilterSeries(action.instance)); + 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)); + case success(FETCH_LIST): + const { data } = action; + return state.set("list", List(data.map(CustomMetric))); + } + return state; +} + +export default mergeReducers( + reducer, + createRequestReducer({ + [ ROOT_KEY ]: FETCH_LIST, + fetch: FETCH, + }), +); + + +const filterMap = ({value, type, key, operator, source, custom, isEvent }) => ({ + value: Array.isArray(value) ? value: [value], + custom, + type: key, + key, operator, + source, + isEvent +}); + +const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getState) => { + dispatch(actionCreator(...args)); + const filter = getState().getIn([ 'search', 'instance', 'filter' ]).toData(); + filter.filters = filter.filters.map(filterMap); + // console.log('filter', filter) + + // let filter = appliedFilter + // .update('filters', list => list.map(f => f.set('value', f.value || '*')) + // .map(filterMap)); + + // const filter.filters = getState().getIn([ 'instance', 'filter' ]).get('filters').map(filterMap).toJS(); + + return isRoute(ERRORS_ROUTE, window.location.pathname) + ? dispatch(fetchErrorsList(filter)) + : dispatch(fetchSessionList(filter)); +}; + +export const edit = reduceThenFetchResource((instance) => ({ + type: EDIT, + instance, +})); + +export const remove = createRemove(name); + +// export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({ +// type: APPLY, +// filter, +// fromUrl, +// })); + +export const updateSeries = (index, series) => ({ + type: UPDATE, + index, + series, +}); + +export function fetch(id) { + return { + id, + types: array(FETCH), + call: c => c.get(`/errors/${id}`), + } +} + +export function save(instance) { + return { + types: SAVE.array, + call: client => client.post( `/${ name }s`, instance.toSaveData()), + }; +} + +export function fetchList() { + return { + 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/svg/icons/filters/browser.svg b/frontend/app/svg/icons/filters/browser.svg new file mode 100644 index 000000000..9718fa3dc --- /dev/null +++ b/frontend/app/svg/icons/filters/browser.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/clickrage.svg b/frontend/app/svg/icons/filters/clickrage.svg new file mode 100644 index 000000000..3220e339e --- /dev/null +++ b/frontend/app/svg/icons/filters/clickrage.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/code.svg b/frontend/app/svg/icons/filters/code.svg new file mode 100644 index 000000000..c9c41b0d7 --- /dev/null +++ b/frontend/app/svg/icons/filters/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/country.svg b/frontend/app/svg/icons/filters/country.svg new file mode 100644 index 000000000..f72a9967b --- /dev/null +++ b/frontend/app/svg/icons/filters/country.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/device.svg b/frontend/app/svg/icons/filters/device.svg new file mode 100644 index 000000000..b453af4aa --- /dev/null +++ b/frontend/app/svg/icons/filters/device.svg @@ -0,0 +1,7 @@ + + + + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/duration.svg b/frontend/app/svg/icons/filters/duration.svg new file mode 100644 index 000000000..e5ecc83ef --- /dev/null +++ b/frontend/app/svg/icons/filters/duration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/error.svg b/frontend/app/svg/icons/filters/error.svg new file mode 100644 index 000000000..a170d1363 --- /dev/null +++ b/frontend/app/svg/icons/filters/error.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/fetch.svg b/frontend/app/svg/icons/filters/fetch.svg new file mode 100644 index 000000000..40cbbbf86 --- /dev/null +++ b/frontend/app/svg/icons/filters/fetch.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/graphql.svg b/frontend/app/svg/icons/filters/graphql.svg new file mode 100644 index 000000000..86eafbec1 --- /dev/null +++ b/frontend/app/svg/icons/filters/graphql.svg @@ -0,0 +1 @@ +graphql \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/i-cursor.svg b/frontend/app/svg/icons/filters/i-cursor.svg new file mode 100644 index 000000000..ce8709fa8 --- /dev/null +++ b/frontend/app/svg/icons/filters/i-cursor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/input.svg b/frontend/app/svg/icons/filters/input.svg new file mode 100644 index 000000000..256542f8a --- /dev/null +++ b/frontend/app/svg/icons/filters/input.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/link.svg b/frontend/app/svg/icons/filters/link.svg new file mode 100644 index 000000000..ee908ebd0 --- /dev/null +++ b/frontend/app/svg/icons/filters/link.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/location.svg b/frontend/app/svg/icons/filters/location.svg new file mode 100644 index 000000000..499378c6b --- /dev/null +++ b/frontend/app/svg/icons/filters/location.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/os.svg b/frontend/app/svg/icons/filters/os.svg new file mode 100644 index 000000000..7a2781f28 --- /dev/null +++ b/frontend/app/svg/icons/filters/os.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app/svg/icons/filters/phone-laptop.svg b/frontend/app/svg/icons/filters/platform.svg similarity index 100% rename from frontend/app/svg/icons/filters/phone-laptop.svg rename to frontend/app/svg/icons/filters/platform.svg diff --git a/frontend/app/svg/icons/filters/referrer.svg b/frontend/app/svg/icons/filters/referrer.svg new file mode 100644 index 000000000..daa9f0141 --- /dev/null +++ b/frontend/app/svg/icons/filters/referrer.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/resize.svg b/frontend/app/svg/icons/filters/resize.svg new file mode 100644 index 000000000..ca5a645f8 --- /dev/null +++ b/frontend/app/svg/icons/filters/resize.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/border-outer.svg b/frontend/app/svg/icons/filters/rev-id.svg similarity index 100% rename from frontend/app/svg/icons/filters/border-outer.svg rename to frontend/app/svg/icons/filters/rev-id.svg diff --git a/frontend/app/svg/icons/filters/state-action.svg b/frontend/app/svg/icons/filters/state-action.svg new file mode 100644 index 000000000..b4a93969f --- /dev/null +++ b/frontend/app/svg/icons/filters/state-action.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/filters/view.svg b/frontend/app/svg/icons/filters/view.svg new file mode 100644 index 000000000..8feb3ec07 --- /dev/null +++ b/frontend/app/svg/icons/filters/view.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index c7ad79982..3bb9069a5 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -34,10 +34,9 @@ export default Record({ rangeValue, startDate, endDate, - condition: 'then', - sort: undefined, - order: undefined, + sort: '', + order: '', viewed: undefined, consoleLogCount: undefined, @@ -109,8 +108,8 @@ export const defaultFilters = [ { label: 'OS', key: KEYS.USER_OS, type: KEYS.USER_OS, filterKey: KEYS.USER_OS, icon: 'os', isFilter: true }, { label: 'Browser', key: KEYS.USER_BROWSER, type: KEYS.USER_BROWSER, filterKey: KEYS.USER_BROWSER, icon: 'window', isFilter: true }, { label: 'Device', key: KEYS.USER_DEVICE, type: KEYS.USER_DEVICE, filterKey: KEYS.USER_DEVICE, icon: 'device', isFilter: true }, - { label: 'Rev ID', key: KEYS.REVID, type: KEYS.REVID, filterKey: KEYS.REVID, icon: 'filters/border-outer', isFilter: true }, - { label: 'Platform', key: KEYS.PLATFORM, type: KEYS.PLATFORM, filterKey: KEYS.PLATFORM, icon: 'filters/phone-laptop', isFilter: true } + { label: 'Rev ID', key: KEYS.REVID, type: KEYS.REVID, filterKey: KEYS.REVID, icon: 'filters/rev-id', isFilter: true }, + { label: 'Platform', key: KEYS.PLATFORM, type: KEYS.PLATFORM, filterKey: KEYS.PLATFORM, icon: 'filters/platform', isFilter: true }, ] }, { diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index c39cc1d2f..03722fc09 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -5,6 +5,7 @@ export enum FilterType { DURATION = "DURATION", MULTIPLE = "MULTIPLE", COUNTRY = "COUNTRY", + DROPDOWN = "DROPDOWN", }; export enum FilterKey { diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 40f3c406c..bc706668f 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -1,5 +1,8 @@ import Record from 'Types/Record'; import { FilterType, FilterKey } from './filterType' +import { countries } from 'App/constants'; + +const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i })); export const CLICK = 'CLICK'; export const INPUT = 'INPUT'; @@ -47,47 +50,6 @@ const ISSUE_OPTIONS = [ { 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, -// }; - const filterKeys = ['is', 'isNot']; const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith']; const targetFilterKeys = ['on', 'notOn']; @@ -226,39 +188,39 @@ export const booleanOptions = [ export const filtersMap = { [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 }, + [FilterKey.INPUT]: { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/input', isEvent: true }, + [FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/location', isEvent: true }, - [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' }, + [FilterKey.USER_OS]: { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/os' }, + [FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/browser' }, + [FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/device' }, + [FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE, category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/platform' }, + [FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/rev-id' }, - [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' }, + [FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/referrer' }, + [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.NUMBER, category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/duration' }, + [FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.DROPDOWN, category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/country', options: countryOptions }, - [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' }, + [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/console' }, + [FilterKey.ERROR]: { key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/error' }, + [FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.MULTIPLE, category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/fetch' }, + [FilterKey.GRAPHQL]: { key: FilterKey.GRAPHQL, type: FilterType.MULTIPLE, category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/graphql' }, + [FilterKey.STATEACTION]: { key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/state-action' }, - [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' }, + [FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/userid' }, + [FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/userid' }, - [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.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.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.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: 'javascript', 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 }, @@ -281,7 +243,7 @@ export default Record({ isFilter: false, actualValue: '', - operator: 'is', + operator: 'notOn', operatorOptions: [], isEvent: false, index: 0, @@ -295,7 +257,7 @@ export default Record({ // key: filter.type === METADATA ? filter.label : filter.key || filter.type, // || camelCased(filter.type.toLowerCase()), // label: getLabel(filter), // target: Target(target), - operator: getOperatorDefault(key), + // operator: getOperatorDefault(key), // value: target ? target.label : filter.value, // value: typeof value === 'string' ? [value] : value, // icon: filter.type ? getfilterIcon(filter.type) : 'filters/metadata' From 45db3fa1d4eca40e575abe0270af035d0a583227 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 25 Jan 2022 15:03:53 +0530 Subject: [PATCH 5/6] feat(ui) - custom metrics --- .../app/components/BugFinder/DateRange.js | 4 +- .../BugFinder/Filters/SortDropdown.js | 2 +- .../FilterSeries/FilterSeries.tsx | 4 +- .../shared/Filters/FilterItem/FilterItem.tsx | 9 +- .../shared/Filters/FilterList/FilterList.tsx | 96 +++++++++++-------- .../Filters/FilterValue/FilterValue.tsx | 47 +++++++-- .../FilterValueDropdown.tsx | 3 +- .../shared/SessionSearch/SessionSearch.tsx | 31 +++++- frontend/app/constants/index.js | 1 + frontend/app/constants/platformOptions.js | 5 + frontend/app/duck/search.js | 26 ++--- frontend/app/types/filter/filterType.ts | 1 + frontend/app/types/filter/newFilter.js | 6 +- 13 files changed, 162 insertions(+), 73 deletions(-) create mode 100644 frontend/app/constants/platformOptions.js diff --git a/frontend/app/components/BugFinder/DateRange.js b/frontend/app/components/BugFinder/DateRange.js index 60e98ffa1..8f16b5207 100644 --- a/frontend/app/components/BugFinder/DateRange.js +++ b/frontend/app/components/BugFinder/DateRange.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { applyFilter } from 'Duck/filters'; +// import { applyFilter } from 'Duck/filters'; +import { applyFilter } from 'Duck/search'; import { fetchList as fetchFunnelsList } from 'Duck/funnels'; import DateRangeDropdown from 'Shared/DateRangeDropdown'; @@ -12,6 +13,7 @@ import DateRangeDropdown from 'Shared/DateRangeDropdown'; }) export default class DateRange extends React.PureComponent { onDateChange = (e) => { + console.log('onDateChange', e); this.props.fetchFunnelsList(e.rangeValue) this.props.applyFilter(e) } diff --git a/frontend/app/components/BugFinder/Filters/SortDropdown.js b/frontend/app/components/BugFinder/Filters/SortDropdown.js index 80f88a0d7..cdcc2e468 100644 --- a/frontend/app/components/BugFinder/Filters/SortDropdown.js +++ b/frontend/app/components/BugFinder/Filters/SortDropdown.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { Dropdown } from 'semantic-ui-react'; import { Icon } from 'UI'; import { sort } from 'Duck/sessions'; -import { applyFilter } from 'Duck/filters'; +import { applyFilter } from 'Duck/search'; import stl from './sortDropdown.css'; @connect(null, { sort, applyFilter }) diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx b/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx index 2fafe112f..40214855c 100644 --- a/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx @@ -99,9 +99,11 @@ function FilterSeries(props: Props) {
{ series.filter.filters.size > 0 ? ( ): (
Add user event or filter to build the series.
diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index 2b0231da6..97e09d190 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -9,9 +9,10 @@ interface Props { filter: any; // event/filter onUpdate: (filter) => void; onRemoveFilter: () => void; + isFilter?: boolean; } function FitlerItem(props: Props) { - const { filterIndex, filter, onUpdate } = props; + const { isFilter = false, filterIndex, filter, onUpdate } = props; const replaceFilter = (filter) => { onUpdate(filter); @@ -45,17 +46,17 @@ function FitlerItem(props: Props) { return (
-
{filterIndex+1}
+ { !isFilter &&
{filterIndex+1}
}
- +
diff --git a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx index 5bab3e6b4..cd775178d 100644 --- a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx +++ b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx @@ -3,12 +3,17 @@ import FilterItem from '../FilterItem'; import { SegmentSelection } from 'UI'; interface Props { - filters: any[]; // event/filter + // filters: any[]; // event/filter + filter?: any; // event/filter onUpdateFilter: (filterIndex, filter) => void; onRemoveFilter: (filterIndex) => void; + onChangeEventsOrder: (e, { name, value }) => void; } function FilterList(props: Props) { - const { filters } = props; + const { filter } = props; + const filters = filter.filters.toJS() + const hasEvents = filter.filters.filter(i => i.isEvent).size > 0; + const hasFilters = filter.filters.filter(i => !i.isEvent).size > 0; const onRemoveFilter = (filterIndex) => { const newFilters = filters.filter((_filter, i) => { @@ -20,45 +25,56 @@ function FilterList(props: Props) { return (
-
-
EVENTS
-
-
Events Order
- null } - // value={{ value: series.filter.eventsOrder }} - value={{ value: 'and' }} - list={ [ - { name: 'AND', value: 'and' }, - { name: 'OR', value: 'or' }, - { name: 'THEN', value: 'then' }, - ]} - /> -
-
- {filters.map((filter, filterIndex) => ( - props.onUpdateFilter(filterIndex, filter)} - onRemoveFilter={() => onRemoveFilter(filterIndex) } - /> - ))} + { hasEvents && ( + <> +
+
EVENTS
+
+
Events Order
+ null } + value={{ value: filter.eventsOrder }} + // value={{ value: 'and' }} + list={ [ + { name: 'AND', value: 'and' }, + { name: 'OR', value: 'or' }, + { name: 'THEN', value: 'then' }, + ]} + /> +
+
+ {filters.map((filter, filterIndex) => filter.isEvent ? ( + props.onUpdateFilter(filterIndex, filter)} + onRemoveFilter={() => onRemoveFilter(filterIndex) } + /> + ): null)} +
+ + )} - {/*
Filters
- {filters.filter(f => !f.isEvent).map((filter, filterIndex) => ( - props.onUpdateFilter(filterIndex, filter)} - onRemoveFilter={() => onRemoveFilter(filterIndex) } - /> - ))} */} + {hasFilters && ( + <> +
+
FILTERS
+ {filters.map((filter, filterIndex) => !filter.isEvent ? ( + props.onUpdateFilter(filterIndex, filter)} + onRemoveFilter={() => onRemoveFilter(filterIndex) } + /> + ): null)} + + )}
); } diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 4a129c365..2983febc8 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import FilterAutoComplete from '../FilterAutoComplete'; import { FilterType } from 'Types/filter/filterType'; import FilterValueDropdown from '../FilterValueDropdown'; @@ -10,6 +10,7 @@ interface Props { } function FilterValue(props: Props) { const { filter } = props; + const [durationValues, setDurationValues] = useState({ minDuration: 0, maxDuration: 0 }); const onAddValue = () => { const newValues = filter.value.concat("") @@ -31,6 +32,25 @@ function FilterValue(props: Props) { props.onUpdate({ ...filter, value: newValues }) } + const onDurationChange = (newValues) => { + console.log('durationValues', durationValues) + // setDurationValues({ ...durationValues }); + setDurationValues({ ...durationValues, ...newValues }); + } + + const handleBlur = (e) => { + // const { filter, onChange } = props; + if (filter.type === FilterType.DURATION) { + const { maxDuration, minDuration, key } = filter; + if (maxDuration || minDuration) return; + if (maxDuration !== durationValues.maxDuration || + minDuration !== durationValues.minDuration) { + // onChange(e, { name: 'value', value: [this.state.minDuration, this.state.maxDuration] }); + props.onUpdate({ ...filter, value: [durationValues.minDuration, durationValues.maxDuration] }); + } + } + } + const renderValueFiled = (value, valueIndex) => { switch(filter.type) { case FilterType.ISSUE: @@ -43,12 +63,22 @@ function FilterValue(props: Props) { onChange={(e, { name, value }) => onSelect(e, { value }, valueIndex)} /> ) + case FilterType.MULTIPLE_DROPDOWN: + return ( + onSelect(e, { value }, valueIndex)} + /> + ) case FilterType.DURATION: return ( @@ -81,12 +111,17 @@ function FilterValue(props: Props) { ) } } + console.log('durationValues', durationValues) return (
- {filter.value && filter.value.map((value, valueIndex) => ( - renderValueFiled(value, valueIndex) - ))} + { filter.type === FilterType.DURATION ? ( + renderValueFiled(filter.value, 0) + ) : ( + filter.value && filter.value.map((value, valueIndex) => ( + renderValueFiled(value, valueIndex) + )) + )}
); } diff --git a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx index eda66f4ef..248fcabed 100644 --- a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx +++ b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx @@ -11,9 +11,10 @@ interface Props { className?: string; options: any[]; search?: boolean; + multiple?: boolean; } function FilterValueDropdown(props: Props) { - const { search = false, options, onChange, value, className = '' } = props; + const { multiple = false, search = false, options, onChange, value, className = '' } = props; // const options = [] return ( diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx index 7483e70b6..4bd50d609 100644 --- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx +++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx @@ -57,17 +57,40 @@ function SessionSearch(props) { }); } + const onChangeEventsOrder = (e, { name, value }) => { + props.edit({ + ...appliedFilter.toData(), + filter: { + ...appliedFilter.filter.toData(), + eventsOrder: value, + } + }); + } + + const clearSearch = () => { + props.edit({ + ...appliedFilter.toData(), + filter: { + ...appliedFilter.filter.toData(), + filters: [], + } + }); + } + + return (
-
+
-
+
- +
diff --git a/frontend/app/constants/index.js b/frontend/app/constants/index.js index ba6f53cf0..8ba18841a 100644 --- a/frontend/app/constants/index.js +++ b/frontend/app/constants/index.js @@ -10,6 +10,7 @@ export { default as alertConditions } from './alertConditions'; export { default as alertMetrics } from './alertMetrics'; export { default as regions } from './regions'; export { default as links } from './links'; +export { default as platformOptions } from './platformOptions'; export { DAYS as SCHEDULE_DAYS, HOURS as SCHEDULE_HOURS, diff --git a/frontend/app/constants/platformOptions.js b/frontend/app/constants/platformOptions.js new file mode 100644 index 000000000..46747ea2e --- /dev/null +++ b/frontend/app/constants/platformOptions.js @@ -0,0 +1,5 @@ +export default [ + { value: 'desktop', text: 'Desktop' }, + { value: 'mobile', text: 'Mobile' }, + { value: 'tablet', text: 'Tablet' }, +] \ No newline at end of file diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js index fee81b122..0aa2afe78 100644 --- a/frontend/app/duck/search.js +++ b/frontend/app/duck/search.js @@ -26,6 +26,7 @@ const SAVE = saveType(name); const EDIT = editType(name); const REMOVE = removeType(name); const UPDATE = `${name}/UPDATE`; +const APPLY = `${name}/APPLY`; const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`; function chartWrapper(chart = []) { @@ -49,6 +50,13 @@ function reducer(state = initialState, action = {}) { switch (action.type) { case EDIT: return state.set('instance', FilterSeries(action.instance)); + case APPLY: + return action.fromUrl + ? state.set('instance', + Filter(action.filter) + // .set('events', state.getIn([ 'instance', 'events' ])) + ) + : state.mergeIn([ 'instance', 'filter' ], action.filter); case success(SAVE): return state.set([ 'instance' ], CustomMetric(action.data)); case success(REMOVE): @@ -85,13 +93,7 @@ const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getStat dispatch(actionCreator(...args)); const filter = getState().getIn([ 'search', 'instance', 'filter' ]).toData(); filter.filters = filter.filters.map(filterMap); - // console.log('filter', filter) - - // let filter = appliedFilter - // .update('filters', list => list.map(f => f.set('value', f.value || '*')) - // .map(filterMap)); - - // const filter.filters = getState().getIn([ 'instance', 'filter' ]).get('filters').map(filterMap).toJS(); + filter.isNew = true // TODO remove this line return isRoute(ERRORS_ROUTE, window.location.pathname) ? dispatch(fetchErrorsList(filter)) @@ -105,11 +107,11 @@ export const edit = reduceThenFetchResource((instance) => ({ export const remove = createRemove(name); -// export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({ -// type: APPLY, -// filter, -// fromUrl, -// })); +export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({ + type: APPLY, + filter, + fromUrl, +})); export const updateSeries = (index, series) => ({ type: UPDATE, diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index 03722fc09..f1e2c33a9 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -6,6 +6,7 @@ export enum FilterType { MULTIPLE = "MULTIPLE", COUNTRY = "COUNTRY", DROPDOWN = "DROPDOWN", + MULTIPLE_DROPDOWN = "MULTIPLE_DROPDOWN", }; export enum FilterKey { diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index bc706668f..1e27ab5b5 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -1,6 +1,6 @@ import Record from 'Types/Record'; import { FilterType, FilterKey } from './filterType' -import { countries } from 'App/constants'; +import { countries, platformOptions } from 'App/constants'; const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i })); @@ -194,11 +194,11 @@ export const filtersMap = { [FilterKey.USER_OS]: { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/os' }, [FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/browser' }, [FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/device' }, - [FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE, category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/platform' }, + [FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: 'gear', label: 'Platform', operator: 'is', operatorOptions: filterOptions, icon: 'filters/platform', options: platformOptions }, [FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/rev-id' }, [FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/referrer' }, - [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.NUMBER, category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/duration' }, + [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/duration' }, [FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.DROPDOWN, category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/country', options: countryOptions }, [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/console' }, From 2008d3dc2ff339b66f83ef5bc7c6aaed4e2b5d67 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Wed, 26 Jan 2022 00:24:08 +0530 Subject: [PATCH 6/6] feat(ui) - custom metrics --- .../app/components/BugFinder/DateRange.js | 12 +- .../shared/Filters/FilterList/FilterList.tsx | 2 +- .../Filters/FilterModal/FilterModal.tsx | 4 +- .../Filters/FilterValue/FilterValue.tsx | 3 +- .../FilterValueDropdown.css | 41 ++++- .../FilterValueDropdown.tsx | 35 +++-- .../SaveSearchModal/SaveSearchModal.tsx | 4 +- .../shared/SessionSearch/SessionSearch.tsx | 30 +--- frontend/app/duck/search.js | 30 ++-- frontend/app/svg/icons/filters/custom.svg | 1 + frontend/app/types/customMetric.js | 5 - frontend/app/types/filter/filter.js | 22 ++- frontend/app/types/filter/filterType.ts | 11 ++ frontend/app/types/filter/newFilter.js | 140 ++++++++---------- 14 files changed, 193 insertions(+), 147 deletions(-) create mode 100644 frontend/app/svg/icons/filters/custom.svg diff --git a/frontend/app/components/BugFinder/DateRange.js b/frontend/app/components/BugFinder/DateRange.js index 8f16b5207..b9bbd745a 100644 --- a/frontend/app/components/BugFinder/DateRange.js +++ b/frontend/app/components/BugFinder/DateRange.js @@ -5,20 +5,24 @@ import { fetchList as fetchFunnelsList } from 'Duck/funnels'; import DateRangeDropdown from 'Shared/DateRangeDropdown'; @connect(state => ({ - rangeValue: state.getIn([ 'filters', 'appliedFilter', 'rangeValue' ]), - startDate: state.getIn([ 'filters', 'appliedFilter', 'startDate' ]), - endDate: state.getIn([ 'filters', 'appliedFilter', 'endDate' ]), + filter: state.getIn([ 'search', 'instance' ]), + // rangeValue: state.getIn([ 'search', 'instance', 'rangeValue' ]), + // startDate: state.getIn([ 'search', 'instance', 'startDate' ]), + // endDate: state.getIn([ 'search', 'instance', 'endDate' ]), }), { applyFilter, fetchFunnelsList }) export default class DateRange extends React.PureComponent { + onDateChange = (e) => { console.log('onDateChange', e); this.props.fetchFunnelsList(e.rangeValue) this.props.applyFilter(e) } render() { - const { startDate, endDate, rangeValue, className } = this.props; + const { filter: { rangeValue, startDate, endDate }, className } = this.props; + // const { startDate, endDate, rangeValue, className } = this.props; + return ( i.isEvent).size > 0; const hasFilters = filter.filters.filter(i => !i.isEvent).size > 0; diff --git a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx index b2299b2e2..e282ab78f 100644 --- a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx +++ b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx @@ -10,9 +10,9 @@ function FilterModal(props: Props) { const { filters, onFilterClick = () => null } = props; return (
-
+
{filters && Object.keys(filters).map((key) => ( -
+
{key}
{filters[key].map((filter: any) => ( diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 2983febc8..ef327eb7f 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -53,7 +53,6 @@ function FilterValue(props: Props) { const renderValueFiled = (value, valueIndex) => { switch(filter.type) { - case FilterType.ISSUE: case FilterType.DROPDOWN: return ( onSelect(e, { value }, valueIndex)} /> ) + case FilterType.ISSUE: case FilterType.MULTIPLE_DROPDOWN: return ( diff --git a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.css b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.css index 8a2329dbd..eb2c457f7 100644 --- a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.css +++ b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.css @@ -1,3 +1,38 @@ +.wrapper { + border: solid thin $gray-light !important; + border-radius: 3px; + background-color: $gray-lightest !important; + display: flex; + align-items: center; + height: 30px; + + & .right { + height: 28px; + display: flex; + align-items: stretch; + padding: 0; + background-color: $gray-lightest; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + + & div { + /* background-color: red; */ + border-left: solid thin $gray-light !important; + width: 28px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + &:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } + &:hover { + background-color: $gray-light; + } + } + } +} .operatorDropdown { font-weight: 400; height: 30px; @@ -8,9 +43,9 @@ 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; + /* 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 { diff --git a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx index 248fcabed..f2a54afc8 100644 --- a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx +++ b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx @@ -12,22 +12,35 @@ interface Props { options: any[]; search?: boolean; multiple?: boolean; + showCloseButton?: boolean; + showOrButton?: boolean; + onRemoveValue?: () => void; + onAddValue?: () => void; } function FilterValueDropdown(props: Props) { - const { multiple = false, search = false, options, onChange, value, className = '' } = props; + const { multiple = false, search = false, options, onChange, value, className = '', showCloseButton = true, showOrButton = true } = props; // const options = [] return ( - } - /> +
+ } + /> +
+ { showCloseButton &&
} + { showOrButton &&
or
} +
+
); } diff --git a/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx b/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx index e609440c2..29084ec42 100644 --- a/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx +++ b/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { edit, save } from 'Duck/filters'; +import { edit, save } from 'Duck/search'; import { Button, Modal, Form, Icon, Checkbox } from 'UI'; import stl from './SaveSearchModal.css'; @@ -72,7 +72,7 @@ function SaveSearchModal(props: Props) { } export default connect(state => ({ - filter: state.getIn(['filters', 'instance']), + filter: state.getIn(['search', 'instance']), loading: state.getIn([ 'filters', 'saveRequest', 'loading' ]) || state.getIn([ 'filters', 'updateRequest', 'loading' ]), }), { edit, save })(SaveSearchModal); \ No newline at end of file diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx index 4bd50d609..caba99eae 100644 --- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx +++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx @@ -15,18 +15,15 @@ function SessionSearch(props) { const onAddFilter = (filter) => { filter.value = [""] - const newFilters = appliedFilter.filter.filters.concat(filter); + const newFilters = appliedFilter.filters.concat(filter); props.edit({ - ...appliedFilter, - filter: { ...appliedFilter.filter, filters: newFilters, - } }); } const onUpdateFilter = (filterIndex, filter) => { - const newFilters = appliedFilter.filter.filters.map((_filter, i) => { + const newFilters = appliedFilter.filters.map((_filter, i) => { if (i === filterIndex) { return filter; } else { @@ -35,45 +32,30 @@ function SessionSearch(props) { }); props.edit({ - ...appliedFilter.toData(), - filter: { ...appliedFilter.filter, filters: newFilters, - } }); } const onRemoveFilter = (filterIndex) => { - const newFilters = appliedFilter.filter.filters.filter((_filter, i) => { + const newFilters = appliedFilter.filters.filter((_filter, i) => { return i !== filterIndex; }); props.edit({ - ...appliedFilter, - filter: { - ...appliedFilter.filter, - filters: newFilters, - } + filters: newFilters, }); } const onChangeEventsOrder = (e, { name, value }) => { props.edit({ - ...appliedFilter.toData(), - filter: { - ...appliedFilter.filter.toData(), - eventsOrder: value, - } + eventsOrder: value, }); } const clearSearch = () => { props.edit({ - ...appliedFilter.toData(), - filter: { - ...appliedFilter.filter.toData(), filters: [], - } }); } @@ -83,7 +65,7 @@ function SessionSearch(props) {
list.filter(item => item.metricId !== action.id)); case success(FETCH): return state.set("instance", ErrorInfo(action.data)); @@ -79,9 +73,9 @@ export default mergeReducers( }), ); - const filterMap = ({value, type, key, operator, source, custom, isEvent }) => ({ - value: Array.isArray(value) ? value: [value], + // value: Array.isArray(value) ? value: [value], + value: value.filter(i => i !== '' && i !== null), custom, type: key, key, operator, @@ -91,7 +85,7 @@ const filterMap = ({value, type, key, operator, source, custom, isEvent }) => ({ const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getState) => { dispatch(actionCreator(...args)); - const filter = getState().getIn([ 'search', 'instance', 'filter' ]).toData(); + const filter = getState().getIn([ 'search', 'instance']).toData(); filter.filters = filter.filters.map(filterMap); filter.isNew = true // TODO remove this line @@ -130,7 +124,11 @@ export function fetch(id) { export function save(instance) { return { types: SAVE.array, - call: client => client.post( `/${ name }s`, instance.toSaveData()), + call: client => client.post('/saved_search', { + name: instance.name, + filter: instance.filter.toSaveData(), + }), + instance, }; } diff --git a/frontend/app/svg/icons/filters/custom.svg b/frontend/app/svg/icons/filters/custom.svg new file mode 100644 index 000000000..c9c41b0d7 --- /dev/null +++ b/frontend/app/svg/icons/filters/custom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/types/customMetric.js b/frontend/app/types/customMetric.js index e40eddb27..0334ae2e6 100644 --- a/frontend/app/types/customMetric.js +++ b/frontend/app/types/customMetric.js @@ -1,11 +1,6 @@ import Record from 'Types/Record'; import { List } from 'immutable'; -// import { DateTime } from 'luxon'; -// import { validateEmail, validateName } from 'App/validate'; import Filter from 'Types/filter'; -import NewFilter from 'Types/filter'; -// import { Event } from 'Types/filter'; -// import CustomFilter from 'Types/filter/customFilter'; export const FilterSeries = Record({ seriesId: undefined, diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index 3bb9069a5..a9871c793 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -35,8 +35,8 @@ export default Record({ startDate, endDate, - sort: '', - order: '', + sort: 'startTs', + order: 'desc', viewed: undefined, consoleLogCount: undefined, @@ -49,6 +49,24 @@ export default Record({ }, { idKey: 'searchId', methods: { + toSaveData() { + const js = this.toJS(); + js.filters = js.filters.map(filter => { + filter.type = filter.key + + delete filter.category + delete filter.icon + delete filter.operatorOptions + delete filter._key + delete filter.key + return filter; + }); + + delete js.createdAt; + delete js.key; + delete js._key; + return js; + }, toData() { const js = this.toJS(); js.filters = js.filters.map(filter => { diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index f1e2c33a9..b5449b161 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -1,3 +1,13 @@ +export enum FilterCategory { + INTERACTIONS = "Interactions", + GEAR = "Gear", + RECORDING_ATTRIBUTES = "Recording Attributes", + JAVASCRIPT = "Javascript", + USER = "User", + METADATA = "Metadata", + PERFORMANCE = "Performance", +}; + export enum FilterType { ISSUE = "ISSUE", BOOLEAN = "BOOLEAN", @@ -48,4 +58,5 @@ export enum FilterKey { TTFB = "TTFB", AVG_CPU_LOAD = "AVG_CPU_LOAD", AVG_MEMORY_USAGE = "AVG_MEMORY_USAGE", + FETCH_FAILED = "FETCH_FAILED", } \ No newline at end of file diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 1e27ab5b5..07628ffe2 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -1,58 +1,27 @@ import Record from 'Types/Record'; -import { FilterType, FilterKey } from './filterType' +import { FilterType, FilterKey, FilterCategory } from './filterType' import { countries, platformOptions } from 'App/constants'; const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i })); -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'; - -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'; - - -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'; - const ISSUE_OPTIONS = [ { text: 'Click Range', value: 'click_rage' }, { text: 'Dead Click', value: 'dead_click' }, + { text: 'Excessive Scrolling', value: 'excessive_scrolling' }, + { text: 'Bad Request', value: 'bad_request' }, + { text: 'Missing Resource', value: 'missing_resource' }, + { text: 'Memory', value: 'memory' }, + { text: 'CPU', value: 'cpu' }, + { text: 'Slow Resource', value: 'slow_resource' }, + { text: 'Slow Page Load', value: 'slow_page_load' }, + { text: 'Crash', value: 'crash' }, + { text: 'Custom', value: 'custom' }, + { text: 'JS Exception', value: 'js_exception' }, ] const filterKeys = ['is', 'isNot']; -const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith']; -const targetFilterKeys = ['on', 'notOn']; +const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith', 'notContains']; +const targetFilterKeys = ['on', 'notOn', 'onAny']; const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp']; const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast']; @@ -78,9 +47,9 @@ const options = [ text: 'contains', value: 'contains' }, { - key: 'doesNotContain', - text: 'does not contain', - value: 'doesNotContain' + key: 'notContains', + text: 'not contains', + value: 'notContains' }, { key: 'hasAnyValue', text: 'has any value', @@ -172,9 +141,9 @@ const options = [ { - key: 'onAnything', - text: 'on anything', - value: 'onAnything' + key: 'onAny', + text: 'on any', + value: 'onAny' } ]; @@ -186,41 +155,59 @@ export const booleanOptions = [ { key: 'false', text: 'false', value: 'false' }, ] +export const customOperators = [ + { key: '=', text: '=', value: '=' }, + { key: '<', text: '<', value: '<' }, + { key: '>', text: '>', value: '>' }, + { key: '<=', text: '<=', value: '<=' }, + { key: '>=', text: '>=', value: '>=' }, +] + export const filtersMap = { - [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/input', isEvent: true }, - [FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/location', isEvent: true }, + [FilterKey.CLICK]: { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click', isEvent: true }, + [FilterKey.INPUT]: { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/input', isEvent: true }, + [FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/location', isEvent: true }, + [FilterKey.CUSTOM]: { key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/custom', isEvent: true }, + [FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/fetch', isEvent: true }, + [FilterKey.GRAPHQL]: { key: FilterKey.GRAPHQL, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/graphql', isEvent: true }, + [FilterKey.STATEACTION]: { key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/state-action', isEvent: true }, + [FilterKey.ERROR]: { key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/error', isEvent: true }, + // [FilterKey.METADATA]: { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/metadata', isEvent: true }, - [FilterKey.USER_OS]: { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/os' }, - [FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/browser' }, - [FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/device' }, - [FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: 'gear', label: 'Platform', operator: 'is', operatorOptions: filterOptions, icon: 'filters/platform', options: platformOptions }, - [FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/rev-id' }, + [FilterKey.USER_OS]: { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/os' }, + [FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/browser' }, + [FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/device' }, + [FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions, icon: 'filters/platform', options: platformOptions }, + [FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/rev-id' }, - [FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/referrer' }, - [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/duration' }, - [FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.DROPDOWN, category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/country', options: countryOptions }, + [FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/referrer' }, + [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/duration' }, + [FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.DROPDOWN, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/country', options: countryOptions }, - [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/console' }, - [FilterKey.ERROR]: { key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/error' }, - [FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.MULTIPLE, category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/fetch' }, - [FilterKey.GRAPHQL]: { key: FilterKey.GRAPHQL, type: FilterType.MULTIPLE, category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/graphql' }, - [FilterKey.STATEACTION]: { key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/state-action' }, - - [FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/userid' }, - [FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/userid' }, + [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/console' }, + + + + + [FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/userid' }, + [FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/userid' }, + + [FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, sourcesourceOperatorOptions: customOperators, source: [], icon: 'filters/click', isEvent: true }, + [FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true }, + // [FilterKey.TIME_BETWEEN_EVENTS]: { key: FilterKey.TIME_BETWEEN_EVENTS, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' }, + [FilterKey.TTFB]: { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'is', operatorOptions: stringFilterOptions, sourceOperatorOptions: customOperators, source: [], icon: 'filters/click', isEvent: true }, + [FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, sourceOperatorOptions: customOperators, source: [], icon: 'filters/click', isEvent: true }, + [FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, sourceOperatorOptions: customOperators, source: [], icon: 'filters/click', isEvent: true }, + [FilterKey.FETCH_FAILED]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Fetch Failed', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true }, + - // [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: 'javascript', label: 'Issue', operator: 'onAnything', operatorOptions: filterOptions, icon: 'filters/click', options: ISSUE_OPTIONS }, + [FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', 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 }, @@ -233,6 +220,7 @@ export default Record({ icon: '', type: '', value: [""], + source: [""], category: '', custom: '', @@ -243,8 +231,10 @@ export default Record({ isFilter: false, actualValue: '', - operator: 'notOn', + operator: '', + sourceOperator: '=', operatorOptions: [], + sourceOptions: [], isEvent: false, index: 0, options: [],