feat(ui) - filters - saved search and other fixes

This commit is contained in:
Shekar Siri 2022-01-28 21:35:17 +05:30
parent 5b256546eb
commit 1d1ebe7d96
10 changed files with 132 additions and 58 deletions

View file

@ -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>

View file

@ -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);

View file

@ -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);

View file

@ -0,0 +1,4 @@
.disabled {
opacity: 0.5 !important;
pointer-events: none;
}

View file

@ -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);

View file

@ -4,4 +4,12 @@
z-index: 999;
display: flex;
flex-direction: column;
max-height: 250px;
overflow-y: auto;
}
.rowItem {
&:hover {
color: $teal;
}
}

View file

@ -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);

View file

@ -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>

View file

@ -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,
});
}

View file

@ -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();