feat(ui) - filters - saved search and other fixes
This commit is contained in:
parent
5b256546eb
commit
1d1ebe7d96
10 changed files with 132 additions and 58 deletions
|
|
@ -31,7 +31,7 @@ import SessionSearchField from 'Shared/SessionSearchField'
|
|||
import SavedSearch from 'Shared/SavedSearch'
|
||||
import LiveSessionList from './LiveSessionList'
|
||||
import SessionSearch from 'Shared/SessionSearch';
|
||||
import { edit as editSearch } from 'Duck/search';
|
||||
import { clearSearch } from 'Duck/search';
|
||||
import { Button } from 'UI';
|
||||
|
||||
const weakEqual = (val1, val2) => {
|
||||
|
|
@ -82,7 +82,7 @@ const allowedQueryKeys = [
|
|||
resetFunnel,
|
||||
resetFunnelFilters,
|
||||
setFunnelPage,
|
||||
editSearch,
|
||||
clearSearch,
|
||||
})
|
||||
@withPageTitle("Sessions - OpenReplay")
|
||||
export default class BugFinder extends React.PureComponent {
|
||||
|
|
@ -181,7 +181,7 @@ export default class BugFinder extends React.PureComponent {
|
|||
<div style={{ width: "65%", marginRight: "10px"}}><SessionSearchField /></div>
|
||||
<div className="flex items-center" style={{ width: "35%"}}>
|
||||
<SavedSearch />
|
||||
<Button plain className="ml-auto" onClick={() => this.props.editSearch({ filters: List() })}>
|
||||
<Button plain className="ml-auto" onClick={() => this.props.clearSearch()}>
|
||||
<span className="font-medium">Clear</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,14 +6,20 @@ import SaveSearchModal from 'Shared/SaveSearchModal'
|
|||
|
||||
interface Props {
|
||||
filter: any;
|
||||
savedSearch: any;
|
||||
}
|
||||
|
||||
function SaveFilterButton(props: Props) {
|
||||
const { savedSearch } = props;
|
||||
const [showModal, setshowModal] = useState(false)
|
||||
return (
|
||||
<div>
|
||||
{/* <Button onClick={() => setshowModal(true)}>SAVE FILTER</Button> */}
|
||||
<IconButton className="mr-2" onClick={() => setshowModal(true)} primaryText label="SAVE SEARCH" icon="zoom-in" />
|
||||
{ savedSearch ? (
|
||||
<IconButton className="mr-2" onClick={() => setshowModal(true)} primaryText label="UPDATE SEARCH" icon="zoom-in" />
|
||||
) : (
|
||||
<IconButton className="mr-2" onClick={() => setshowModal(true)} primaryText label="SAVE SEARCH" icon="zoom-in" />
|
||||
)}
|
||||
|
||||
<SaveSearchModal
|
||||
show={showModal}
|
||||
closeHandler={() => setshowModal(false)}
|
||||
|
|
@ -23,5 +29,6 @@ function SaveFilterButton(props: Props) {
|
|||
}
|
||||
|
||||
export default connect(state => ({
|
||||
filter: state.getIn([ 'filters', 'appliedFilter' ]),
|
||||
filter: state.getIn([ 'search', 'instance' ]),
|
||||
savedSearch: state.getIn([ 'search', 'savedSearch' ]),
|
||||
}), { save })(SaveFilterButton);
|
||||
|
|
@ -1,34 +1,50 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { edit, save } from 'Duck/search';
|
||||
import { edit, save, remove } from 'Duck/search';
|
||||
import { Button, Modal, Form, Icon, Checkbox } from 'UI';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
import stl from './SaveSearchModal.css';
|
||||
|
||||
interface Props {
|
||||
filter: any;
|
||||
loading: boolean;
|
||||
edit: (filter: any) => void;
|
||||
save: (filter: any) => Promise<void>;
|
||||
save: (searchId, name, filter: any) => Promise<void>;
|
||||
show: boolean;
|
||||
closeHandler: () => void;
|
||||
savedSearch: any;
|
||||
remove: (filterId: number) => Promise<void>;
|
||||
}
|
||||
function SaveSearchModal(props: Props) {
|
||||
const { filter, loading, show, closeHandler } = props;
|
||||
const [name, setName] = useState(props.savedSearch ? props.savedSearch.name : '');
|
||||
const { savedSearch, filter, loading, show, closeHandler } = props;
|
||||
|
||||
const onNameChange = ({ target: { value } }) => {
|
||||
props.edit({ name: value });
|
||||
// props.edit({ name: value });
|
||||
setName(value);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
const { filter, closeHandler } = props;
|
||||
if (filter.name.trim() === '') return;
|
||||
props.save(filter).then(function() {
|
||||
if (name.trim() === '') return;
|
||||
props.save(savedSearch ? savedSearch.searchId : null, name, filter).then(function() {
|
||||
// this.props.fetchFunnelsList();
|
||||
closeHandler();
|
||||
});
|
||||
}
|
||||
|
||||
console.log('filter', filter)
|
||||
const onDelete = async () => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`
|
||||
})) {
|
||||
props.remove(savedSearch.searchId).then(() => {
|
||||
closeHandler();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Modal size="tiny" open={ show }>
|
||||
|
|
@ -52,29 +68,33 @@ function SaveSearchModal(props: Props) {
|
|||
autoFocus={ true }
|
||||
// className={ stl.name }
|
||||
name="name"
|
||||
value={ filter.name }
|
||||
value={ name }
|
||||
onChange={ onNameChange }
|
||||
placeholder="Title"
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Actions className="">
|
||||
<Button
|
||||
primary
|
||||
onClick={ onSave }
|
||||
loading={ loading }
|
||||
>
|
||||
{ filter.exists() ? 'Modify' : 'Save' }
|
||||
</Button>
|
||||
<Button className={ stl.cancelButton } marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
|
||||
<Modal.Actions className="flex items-center">
|
||||
<div className="mr-auto">
|
||||
<Button
|
||||
primary
|
||||
onClick={ onSave }
|
||||
loading={ loading }
|
||||
>
|
||||
{ savedSearch ? 'Update' : 'Create' }
|
||||
</Button>
|
||||
<Button className={ stl.cancelButton } marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
|
||||
</div>
|
||||
{ savedSearch && <Button className={ stl.cancelButton } marginRight onClick={ onDelete }>{ 'Delete' }</Button> }
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
savedSearch: state.getIn([ 'search', 'savedSearch' ]),
|
||||
filter: state.getIn(['search', 'instance']),
|
||||
loading: state.getIn([ 'search', 'saveRequest', 'loading' ]) ||
|
||||
state.getIn([ 'search', 'updateRequest', 'loading' ]),
|
||||
}), { edit, save })(SaveSearchModal);
|
||||
}), { edit, save, remove })(SaveSearchModal);
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.disabled {
|
||||
opacity: 0.5 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -2,14 +2,20 @@ 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 { fetchList as fetchListSavedSearch } from 'Duck/search';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import cn from 'classnames';
|
||||
import { list } from 'App/components/BugFinder/CustomFilters/filterModal.css';
|
||||
import stl from './SavedSearch.css';
|
||||
|
||||
interface Props {
|
||||
fetchListSavedSearch: () => void;
|
||||
list: any;
|
||||
savedSearch: any;
|
||||
}
|
||||
function SavedSearch(props) {
|
||||
const { list } = props;
|
||||
const { savedSearch } = props;
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -22,7 +28,7 @@ function SavedSearch(props) {
|
|||
onClickOutside={() => setShowMenu(false)}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="flex items-center">
|
||||
<div className={cn("flex items-center", { [stl.disabled] : list.size === 0})}>
|
||||
<Button prime outline size="small"
|
||||
className="flex items-center"
|
||||
onClick={() => setShowMenu(true)}
|
||||
|
|
@ -30,12 +36,13 @@ function SavedSearch(props) {
|
|||
<span className="mr-2">Search Saved</span>
|
||||
<Icon name="ellipsis-v" color="teal" size="14" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center ml-2">
|
||||
<Icon name="search" size="14" />
|
||||
<span className="color-gray-medium px-1">Viewing:</span>
|
||||
<span className="font-medium">Login ...</span>
|
||||
</div>
|
||||
{ savedSearch && (
|
||||
<div className="flex items-center ml-2">
|
||||
<Icon name="search" size="14" />
|
||||
<span className="color-gray-medium px-1">Viewing:</span>
|
||||
<span className="font-medium">{savedSearch.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ showMenu && (
|
||||
|
|
@ -52,5 +59,6 @@ function SavedSearch(props) {
|
|||
}
|
||||
|
||||
export default connect(state => ({
|
||||
list: state.getIn([ 'filters', 'list' ]),
|
||||
list: state.getIn([ 'search', 'list' ]),
|
||||
savedSearch: state.getIn([ 'search', 'savedSearch' ])
|
||||
}), { fetchListSavedSearch })(SavedSearch);
|
||||
|
|
@ -4,4 +4,12 @@
|
|||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.rowItem {
|
||||
&:hover {
|
||||
color: $teal;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +1,29 @@
|
|||
import React from 'react';
|
||||
import stl from './SavedSearchDropdown.css';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import { applyFilter, remove } from 'Duck/search'
|
||||
import { applySavedSearch, remove, edit } from 'Duck/search'
|
||||
import { connect } from 'react-redux';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
|
||||
interface Props {
|
||||
list: Array<any>
|
||||
applyFilter: (filter: any) => void
|
||||
applySavedSearch: (filter: any) => void
|
||||
remove: (id: string) => Promise<void>
|
||||
onClose: () => void
|
||||
onClose: () => void,
|
||||
edit: (filter: any) => void,
|
||||
}
|
||||
|
||||
function Row ({ name, onClick, onClickEdit, onDelete }) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="flex items-center cursor-pointer hover:bg-active-blue"
|
||||
className={cn(stl.rowItem, "flex items-center cursor-pointer hover:bg-active-blue")}
|
||||
>
|
||||
<div className="px-3 py-2">{name}</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<div className="cursor-pointer px-2 hover:bg-active-blue" onClick={onClickEdit}><Icon name="pencil" size="14" /></div>
|
||||
<div className="cursor-pointer px-2 hover:bg-active-blue" onClick={onDelete}><Icon name="trash" size="14" /></div>
|
||||
{/* <div className="cursor-pointer px-2 hover:bg-active-blue" onClick={onDelete}><Icon name="trash" size="14" /></div> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -29,7 +31,8 @@ function Row ({ name, onClick, onClickEdit, onDelete }) {
|
|||
|
||||
function SavedSearchDropdown(props: Props) {
|
||||
const onClick = (item) => {
|
||||
props.applyFilter(item.filter)
|
||||
props.applySavedSearch(item)
|
||||
props.edit(item.filter)
|
||||
props.onClose()
|
||||
}
|
||||
|
||||
|
|
@ -64,4 +67,4 @@ function SavedSearchDropdown(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect(null, { applyFilter, remove })(SavedSearchDropdown);
|
||||
export default connect(null, { applySavedSearch, remove, edit })(SavedSearchDropdown);
|
||||
|
|
@ -84,7 +84,6 @@ function SessionSearch(props) {
|
|||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<SaveFilterButton />
|
||||
{/* <Button plain>SAVE FUNNEL</Button> */}
|
||||
<IconButton primaryText label="SAVE FUNNEL" icon="filter" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { fetchList as fetchErrorsList } from './errors';
|
|||
const ERRORS_ROUTE = errorsRoute();
|
||||
|
||||
const name = "search";
|
||||
const idKey = "metricId";
|
||||
const idKey = "searchId";
|
||||
|
||||
const FETCH_LIST = fetchListType(name);
|
||||
const FETCH_FILTER_SEARCH = fetchListType(`${name}/FILTER_SEARCH`);
|
||||
|
|
@ -22,6 +22,8 @@ const FETCH = fetchType(name);
|
|||
const SAVE = saveType(name);
|
||||
const EDIT = editType(name);
|
||||
const REMOVE = removeType(name);
|
||||
const APPLY_SAVED_SEARCH = `${name}/APPLY_SAVED_SEARCH`;
|
||||
const CLEAR_SEARCH = `${name}/CLEAR_SEARCH`;
|
||||
const UPDATE = `${name}/UPDATE`;
|
||||
const APPLY = `${name}/APPLY`;
|
||||
const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`;
|
||||
|
|
@ -30,16 +32,17 @@ 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 savedSearchIdKey = 'searchId'
|
||||
const updateItemInList = createListUpdater(savedSearchIdKey);
|
||||
const updateInstance = (state, instance) => state.getIn([ "savedSearch", savedSearchIdKey ]) === instance[savedSearchIdKey]
|
||||
? state.mergeIn([ "savedSearch" ], instance)
|
||||
: state;
|
||||
|
||||
const initialState = Map({
|
||||
list: List(),
|
||||
alertMetricId: null,
|
||||
instance: new Filter({ filters: [] }),
|
||||
savedFilter: new SavedFilter({ filters: [] }),
|
||||
savedSearch: null,
|
||||
filterSearchList: List(),
|
||||
});
|
||||
|
||||
|
|
@ -56,16 +59,19 @@ function reducer(state = initialState, action = {}) {
|
|||
)
|
||||
: state.mergeIn(['instance'], action.filter);
|
||||
case success(SAVE):
|
||||
return state.mergeIn([ 'instance' ], action.data);
|
||||
return updateItemInList(updateInstance(state, action.data), action.data);
|
||||
// return state.mergeIn([ 'instance' ], action.data);
|
||||
case success(REMOVE):
|
||||
return state.update('list', list => list.filter(item => item.metricId !== action.id));
|
||||
return state.update('list', list => list.filter(item => item.searchId !== 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(NewFilter)));
|
||||
return state.set("list", List(data.map(SavedFilter)));
|
||||
case success(FETCH_FILTER_SEARCH):
|
||||
return state.set("filterSearchList", action.data.map(NewFilter));
|
||||
case APPLY_SAVED_SEARCH:
|
||||
return state.set('savedSearch', action.filter);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
|
@ -106,7 +112,7 @@ export const edit = reduceThenFetchResource((instance) => ({
|
|||
instance,
|
||||
}));
|
||||
|
||||
export const remove = createRemove(name);
|
||||
export const remove = createRemove(name, (id) => `/saved_search/${id}`);
|
||||
|
||||
export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({
|
||||
type: APPLY,
|
||||
|
|
@ -114,6 +120,16 @@ export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({
|
|||
fromUrl,
|
||||
}));
|
||||
|
||||
export const applySavedSearch = (filter) => (dispatch, getState) => {
|
||||
// console.log('applySavedSearch', filter);
|
||||
// export const applySavedSearch = (filter) => ({
|
||||
dispatch(edit(filter ? filter.filter : new Filter({ fitlers: []})));
|
||||
return dispatch({
|
||||
type: APPLY_SAVED_SEARCH,
|
||||
filter,
|
||||
})
|
||||
};
|
||||
|
||||
export const updateSeries = (index, series) => ({
|
||||
type: UPDATE,
|
||||
index,
|
||||
|
|
@ -128,11 +144,12 @@ export function fetch(id) {
|
|||
}
|
||||
}
|
||||
|
||||
export function save(instance) {
|
||||
export function save(id, name, instance) {
|
||||
instance = instance instanceof SavedFilter ? instance : new SavedFilter(instance);
|
||||
return {
|
||||
types: SAVE.array,
|
||||
call: client => client.post('/saved_search', {
|
||||
name: instance.name,
|
||||
call: client => client.post(!id ? '/saved_search' : `/saved_search/${id}`, {
|
||||
name: name,
|
||||
filter: instance.toSaveData(),
|
||||
}),
|
||||
instance,
|
||||
|
|
@ -159,4 +176,12 @@ export function fetchFilterSearch(params) {
|
|||
call: client => client.get('/events/search', params),
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
export const clearSearch = () => (dispatch, getState) => {
|
||||
dispatch(applySavedSearch(null));
|
||||
dispatch(edit(new Filter({ filters: [] })));
|
||||
return dispatch({
|
||||
type: CLEAR_SEARCH,
|
||||
});
|
||||
}
|
||||
|
|
@ -3,16 +3,16 @@ import Filter from './filter';
|
|||
import { List } from 'immutable';
|
||||
|
||||
export default Record({
|
||||
filterId: undefined,
|
||||
searchId: undefined,
|
||||
projectId: undefined,
|
||||
userId: undefined,
|
||||
name: undefined,
|
||||
name: '',
|
||||
filter: Filter(),
|
||||
createdAt: undefined,
|
||||
count: 0,
|
||||
watchdogs: List()
|
||||
}, {
|
||||
idKey: 'filterId',
|
||||
idKey: 'searchId',
|
||||
methods: {
|
||||
toData() {
|
||||
const js = this.toJS();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue