From 155e7e43314d8c9346e47d1f031a20e89f51c10f Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 17 Jan 2022 17:06:01 +0530 Subject: [PATCH] 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,