change(ui): remove bugfinder components and store
This commit is contained in:
parent
3dc3d736ac
commit
22cdb4f4e7
41 changed files with 4 additions and 1231 deletions
|
|
@ -11,14 +11,12 @@ const withProvider = (story) => (
|
||||||
|
|
||||||
// const req = require.context('../app/components/ui', true, /\.stories\.js$/);
|
// const req = require.context('../app/components/ui', true, /\.stories\.js$/);
|
||||||
// const issues = require.context('../app/components/Session/Issues', true, /\.stories\.js$/);
|
// const issues = require.context('../app/components/Session/Issues', true, /\.stories\.js$/);
|
||||||
// const bugFinder = require.context('../app/components/BugFinder', true, /\.stories\.js$/);
|
|
||||||
|
|
||||||
addDecorator(withProvider);
|
addDecorator(withProvider);
|
||||||
addDecorator(story => <MemoryRouter initialEntries={['/']}>{story()}</MemoryRouter>);
|
addDecorator(story => <MemoryRouter initialEntries={['/']}>{story()}</MemoryRouter>);
|
||||||
|
|
||||||
// function loadStories() {
|
// function loadStories() {
|
||||||
// req.keys().forEach(filename => req(filename));
|
// req.keys().forEach(filename => req(filename));
|
||||||
// bugFinder.keys().forEach(filename => bugFinder(filename));
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// configure(loadStories, module);
|
// configure(loadStories, module);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const LiveSessionPure = lazy(() => import('Components/Session/LiveSession'));
|
||||||
const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding'));
|
const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding'));
|
||||||
const ClientPure = lazy(() => import('Components/Client/Client'));
|
const ClientPure = lazy(() => import('Components/Client/Client'));
|
||||||
const AssistPure = lazy(() => import('Components/Assist'));
|
const AssistPure = lazy(() => import('Components/Assist'));
|
||||||
const BugFinderPure = lazy(() => import('Components/Overview'));
|
const SessionsOverviewPure = lazy(() => import('Components/Overview'));
|
||||||
const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard'));
|
const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard'));
|
||||||
const ErrorsPure = lazy(() => import('Components/Errors/Errors'));
|
const ErrorsPure = lazy(() => import('Components/Errors/Errors'));
|
||||||
const FunnelDetailsPure = lazy(() => import('Components/Funnels/FunnelDetails'));
|
const FunnelDetailsPure = lazy(() => import('Components/Funnels/FunnelDetails'));
|
||||||
|
|
@ -37,7 +37,7 @@ const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDeta
|
||||||
const FunnelPagePure = lazy(() => import('Components/Funnels/FunnelPage'));
|
const FunnelPagePure = lazy(() => import('Components/Funnels/FunnelPage'));
|
||||||
const MultiviewPure = lazy(() => import('Components/Session_/Multiview/Multiview.tsx'));
|
const MultiviewPure = lazy(() => import('Components/Session_/Multiview/Multiview.tsx'));
|
||||||
|
|
||||||
const BugFinder = withSiteIdUpdater(BugFinderPure);
|
const SessionsOverview = withSiteIdUpdater(SessionsOverviewPure);
|
||||||
const Dashboard = withSiteIdUpdater(DashboardPure);
|
const Dashboard = withSiteIdUpdater(DashboardPure);
|
||||||
const Session = withSiteIdUpdater(SessionPure);
|
const Session = withSiteIdUpdater(SessionPure);
|
||||||
const LiveSession = withSiteIdUpdater(LiveSessionPure);
|
const LiveSession = withSiteIdUpdater(LiveSessionPure);
|
||||||
|
|
@ -235,7 +235,7 @@ class Router extends React.Component {
|
||||||
<Route exact strict path={withSiteId(FUNNEL_PATH, siteIdList)} component={FunnelPage} />
|
<Route exact strict path={withSiteId(FUNNEL_PATH, siteIdList)} component={FunnelPage} />
|
||||||
<Route exact strict path={withSiteId(FUNNEL_CREATE_PATH, siteIdList)} component={FunnelsDetails} />
|
<Route exact strict path={withSiteId(FUNNEL_CREATE_PATH, siteIdList)} component={FunnelsDetails} />
|
||||||
<Route exact strict path={withSiteId(FUNNEL_ISSUE_PATH, siteIdList)} component={FunnelIssue} />
|
<Route exact strict path={withSiteId(FUNNEL_ISSUE_PATH, siteIdList)} component={FunnelIssue} />
|
||||||
<Route exact strict path={withSiteId(SESSIONS_PATH, siteIdList)} component={BugFinder} />
|
<Route exact strict path={withSiteId(SESSIONS_PATH, siteIdList)} component={SessionsOverview} />
|
||||||
<Route exact strict path={withSiteId(SESSION_PATH, siteIdList)} component={Session} />
|
<Route exact strict path={withSiteId(SESSION_PATH, siteIdList)} component={Session} />
|
||||||
<Route exact strict path={withSiteId(LIVE_SESSION_PATH, siteIdList)} component={LiveSession} />
|
<Route exact strict path={withSiteId(LIVE_SESSION_PATH, siteIdList)} component={LiveSession} />
|
||||||
<Route exact strict path={withSiteId(LIVE_SESSION_PATH, siteIdList)} render={(props) => <Session {...props} live />} />
|
<Route exact strict path={withSiteId(LIVE_SESSION_PATH, siteIdList)} render={(props) => <Session {...props} live />} />
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,6 @@ export default class APIClient {
|
||||||
if (
|
if (
|
||||||
path !== '/targets_temp' &&
|
path !== '/targets_temp' &&
|
||||||
!path.includes('/metadata/session_search') &&
|
!path.includes('/metadata/session_search') &&
|
||||||
!path.includes('/watchdogs/rules') &&
|
|
||||||
!path.includes('/assist/credentials') &&
|
!path.includes('/assist/credentials') &&
|
||||||
!!this.siteId &&
|
!!this.siteId &&
|
||||||
siteIdRequiredPaths.some(sidPath => path.startsWith(sidPath))
|
siteIdRequiredPaths.some(sidPath => path.startsWith(sidPath))
|
||||||
|
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import APIClient from 'App/api_client';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { Input, Icon } from 'UI';
|
|
||||||
import { debounce } from 'App/utils';
|
|
||||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
|
||||||
import EventSearchInput from 'Shared/EventSearchInput';
|
|
||||||
import stl from './autoComplete.module.css';
|
|
||||||
import FilterItem from '../CustomFilters/FilterItem';
|
|
||||||
|
|
||||||
const TYPE_TO_SEARCH_MSG = "Start typing to search...";
|
|
||||||
const NO_RESULTS_MSG = "No results found.";
|
|
||||||
const SOME_ERROR_MSG = "Some error occured.";
|
|
||||||
const defaultValueToText = value => value;
|
|
||||||
const defaultOptionMapping = (values, valueToText) => values.map(value => ({ text: valueToText(value), value }));
|
|
||||||
|
|
||||||
const hiddenStyle = {
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
opacity: 0, position: 'fixed', left: '-3000px'
|
|
||||||
};
|
|
||||||
|
|
||||||
let pasted = false;
|
|
||||||
let changed = false;
|
|
||||||
|
|
||||||
class AutoComplete extends React.PureComponent {
|
|
||||||
static defaultProps = {
|
|
||||||
method: 'GET',
|
|
||||||
params: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
values: [],
|
|
||||||
noResultsMessage: TYPE_TO_SEARCH_MSG,
|
|
||||||
ddOpen: false,
|
|
||||||
query: this.props.value,
|
|
||||||
loading: false,
|
|
||||||
error: false
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(newProps) {
|
|
||||||
if (this.props.value !== newProps.value) {
|
|
||||||
this.setState({ query: newProps.value});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickOutside = () => {
|
|
||||||
this.setState({ ddOpen: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
requestValues = (q) => {
|
|
||||||
const { params, endpoint, method } = this.props;
|
|
||||||
this.setState({
|
|
||||||
loading: true,
|
|
||||||
error: false,
|
|
||||||
});
|
|
||||||
return new APIClient()[ method.toLowerCase() ](endpoint, { ...params, q })
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(({ errors, data }) => {
|
|
||||||
if (errors) {
|
|
||||||
this.setError();
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
ddOpen: true,
|
|
||||||
values: data,
|
|
||||||
loading: false,
|
|
||||||
noResultsMessage: NO_RESULTS_MSG,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(this.setError);
|
|
||||||
}
|
|
||||||
|
|
||||||
debouncedRequestValues = debounce(this.requestValues, 1000)
|
|
||||||
|
|
||||||
setError = () => this.setState({
|
|
||||||
loading: false,
|
|
||||||
error: true,
|
|
||||||
noResultsMessage: SOME_ERROR_MSG,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
onInputChange = ({ target: { value } }) => {
|
|
||||||
changed = true;
|
|
||||||
this.setState({ query: value, updated: true })
|
|
||||||
const _value = value ? value.trim() : undefined;
|
|
||||||
if (_value !== '' && _value !== ' ') {
|
|
||||||
this.debouncedRequestValues(_value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur = ({ target: { value } }) => {
|
|
||||||
// to avoid sending unnecessary request on focus in/out without changing
|
|
||||||
if (!changed && !pasted) return;
|
|
||||||
|
|
||||||
value = pasted ? this.hiddenInput.value : value;
|
|
||||||
const { onSelect, name } = this.props;
|
|
||||||
if (value !== this.props.value) {
|
|
||||||
const _value = value ? value.trim() : undefined;
|
|
||||||
onSelect(null, {name, value: _value});
|
|
||||||
}
|
|
||||||
|
|
||||||
changed = false;
|
|
||||||
pasted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemClick = (e, item) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
const { onSelect, name } = this.props;
|
|
||||||
|
|
||||||
this.setState({ query: item.value, ddOpen: false})
|
|
||||||
onSelect(e, {name, ...item.toJS()});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { ddOpen, query, loading, values } = this.state;
|
|
||||||
const {
|
|
||||||
optionMapping = defaultOptionMapping,
|
|
||||||
valueToText = defaultValueToText,
|
|
||||||
placeholder = 'Type to search...',
|
|
||||||
headerText = '',
|
|
||||||
fullWidth = false,
|
|
||||||
onRemoveValue = () => {},
|
|
||||||
onAddValue = () => {},
|
|
||||||
showCloseButton = false,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const options = optionMapping(values, valueToText)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OutsideClickDetectingDiv
|
|
||||||
className={ cn("relative flex items-center", { "flex-1" : fullWidth }) }
|
|
||||||
onClickOutside={this.onClickOutside}
|
|
||||||
>
|
|
||||||
{/* <EventSearchInput /> */}
|
|
||||||
<div className={stl.inputWrapper}>
|
|
||||||
<input
|
|
||||||
name="query"
|
|
||||||
// className={cn(stl.input)}
|
|
||||||
onFocus={ () => this.setState({ddOpen: true})}
|
|
||||||
onChange={ this.onInputChange }
|
|
||||||
onBlur={ this.onBlur }
|
|
||||||
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
|
|
||||||
} }
|
|
||||||
autocomplete="do-not-autofill-bad-chrome"
|
|
||||||
/>
|
|
||||||
<div className={stl.right} onClick={showCloseButton ? onRemoveValue : onAddValue}>
|
|
||||||
{ showCloseButton ? <Icon name="close" size="14" /> : <span className="px-1">or</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showCloseButton && <div className='ml-2'>or</div>}
|
|
||||||
{/* <Input
|
|
||||||
className={ cn(stl.searchInput, { [ stl.fullWidth] : fullWidth }) }
|
|
||||||
onChange={ this.onInputChange }
|
|
||||||
onBlur={ this.onBlur }
|
|
||||||
onFocus={ () => this.setState({ddOpen: true})}
|
|
||||||
value={ query }
|
|
||||||
// icon="search"
|
|
||||||
label={{ basic: true, content: <div>test</div> }}
|
|
||||||
labelPosition='right'
|
|
||||||
loading={ loading }
|
|
||||||
autoFocus={ true }
|
|
||||||
type="search"
|
|
||||||
placeholder={ placeholder }
|
|
||||||
onPaste={(e) => {
|
|
||||||
const text = e.clipboardData.getData('Text');
|
|
||||||
this.hiddenInput.value = text;
|
|
||||||
pasted = true; // to use only the hidden input
|
|
||||||
} }
|
|
||||||
/> */}
|
|
||||||
<textarea style={hiddenStyle} ref={(ref) => this.hiddenInput = ref }></textarea>
|
|
||||||
{ ddOpen && options.length > 0 &&
|
|
||||||
<div className={ stl.menu }>
|
|
||||||
{ headerText && headerText }
|
|
||||||
{
|
|
||||||
options.map(item => (
|
|
||||||
<FilterItem
|
|
||||||
label={ item.value }
|
|
||||||
icon={ item.icon }
|
|
||||||
onClick={ (e) => this.onItemClick(e, item) }
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</OutsideClickDetectingDiv>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutoComplete;
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import stl from './dropdownItem.module.css';
|
|
||||||
|
|
||||||
const DropdownItem = ({ value, onSelect }) => {
|
|
||||||
return (
|
|
||||||
<div className={ stl.wrapper } onClick={ onSelect } >{ value }</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DropdownItem;
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchInput {
|
|
||||||
& input {
|
|
||||||
font-size: 13px !important;
|
|
||||||
padding: 5px !important;
|
|
||||||
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;
|
|
||||||
color: $gray-darkest !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
.wrapper {
|
|
||||||
padding: 8px;
|
|
||||||
border-bottom: solid thin rgba(0, 0, 0, 0.05);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
background-color: $active-blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './AutoComplete';
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import withPageTitle from 'HOCs/withPageTitle';
|
|
||||||
import { fetchFavoriteList as fetchFavoriteSessionList } from 'Duck/sessions';
|
|
||||||
import { applyFilter, clearEvents, addAttribute } from 'Duck/filters';
|
|
||||||
import { KEYS } from 'Types/filter/customFilter';
|
|
||||||
import SessionList from './SessionList';
|
|
||||||
import stl from './bugFinder.module.css';
|
|
||||||
import withLocationHandlers from 'HOCs/withLocationHandlers';
|
|
||||||
import { fetch as fetchFilterVariables } from 'Duck/sources';
|
|
||||||
import { fetchSources } from 'Duck/customField';
|
|
||||||
import { setActiveTab } from 'Duck/search';
|
|
||||||
import SessionsMenu from './SessionsMenu/SessionsMenu';
|
|
||||||
import NoSessionsMessage from 'Shared/NoSessionsMessage';
|
|
||||||
import SessionSearch from 'Shared/SessionSearch';
|
|
||||||
import MainSearchBar from 'Shared/MainSearchBar';
|
|
||||||
import { clearSearch, fetchSessions, addFilterByKeyAndValue } from 'Duck/search';
|
|
||||||
import { FilterKey } from 'Types/filter/filterType';
|
|
||||||
|
|
||||||
const weakEqual = (val1, val2) => {
|
|
||||||
if (!!val1 === false && !!val2 === false) return true;
|
|
||||||
if (!val1 !== !val2) return false;
|
|
||||||
return `${val1}` === `${val2}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const allowedQueryKeys = [
|
|
||||||
'userOs',
|
|
||||||
'userId',
|
|
||||||
'userBrowser',
|
|
||||||
'userDevice',
|
|
||||||
'userCountry',
|
|
||||||
'startDate',
|
|
||||||
'endDate',
|
|
||||||
'minDuration',
|
|
||||||
'maxDuration',
|
|
||||||
'referrer',
|
|
||||||
'sort',
|
|
||||||
'order',
|
|
||||||
];
|
|
||||||
|
|
||||||
@withLocationHandlers()
|
|
||||||
@connect(
|
|
||||||
(state) => ({
|
|
||||||
filter: state.getIn(['filters', 'appliedFilter']),
|
|
||||||
variables: state.getIn(['customFields', 'list']),
|
|
||||||
sources: state.getIn(['customFields', 'sources']),
|
|
||||||
filterValues: state.get('filterValues'),
|
|
||||||
favoriteList: state.getIn(['sessions', 'favoriteList']),
|
|
||||||
currentProjectId: state.getIn(['site', 'siteId']),
|
|
||||||
sites: state.getIn(['site', 'list']),
|
|
||||||
watchdogs: state.getIn(['watchdogs', 'list']),
|
|
||||||
activeFlow: state.getIn(['filters', 'activeFlow']),
|
|
||||||
sessions: state.getIn(['sessions', 'list']),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
fetchFavoriteSessionList,
|
|
||||||
applyFilter,
|
|
||||||
addAttribute,
|
|
||||||
fetchFilterVariables,
|
|
||||||
fetchSources,
|
|
||||||
clearEvents,
|
|
||||||
setActiveTab,
|
|
||||||
clearSearch,
|
|
||||||
fetchSessions,
|
|
||||||
addFilterByKeyAndValue,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@withPageTitle('Sessions - OpenReplay')
|
|
||||||
export default class BugFinder extends React.PureComponent {
|
|
||||||
state = { showRehydratePanel: false };
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
// TODO should cache the response
|
|
||||||
// props.fetchSources().then(() => {
|
|
||||||
// defaultFilters[6] = {
|
|
||||||
// category: 'Collaboration',
|
|
||||||
// type: 'CUSTOM',
|
|
||||||
// keys: this.props.sources.filter(({type}) => type === 'collaborationTool').map(({ label, key }) => ({ type: 'CUSTOM', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS()
|
|
||||||
// };
|
|
||||||
// defaultFilters[7] = {
|
|
||||||
// category: 'Logging Tools',
|
|
||||||
// type: 'ERROR',
|
|
||||||
// keys: this.props.sources.filter(({type}) => type === 'logTool').map(({ label, key }) => ({ type: 'ERROR', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS()
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
// if (props.sessions.size === 0) {
|
|
||||||
// props.fetchSessions();
|
|
||||||
// }
|
|
||||||
|
|
||||||
const queryFilter = this.props.query.all(allowedQueryKeys);
|
|
||||||
if (queryFilter.hasOwnProperty('userId')) {
|
|
||||||
props.addFilterByKeyAndValue(FilterKey.USERID, queryFilter.userId);
|
|
||||||
} else {
|
|
||||||
if (props.sessions.size === 0) {
|
|
||||||
props.fetchSessions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleRehydratePanel = () => {
|
|
||||||
this.setState({ showRehydratePanel: !this.state.showRehydratePanel });
|
|
||||||
};
|
|
||||||
|
|
||||||
setActiveTab = (tab) => {
|
|
||||||
this.props.setActiveTab(tab);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { showRehydratePanel } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="page-margin container-90 flex relative">
|
|
||||||
<div className="flex-1 flex">
|
|
||||||
<div className="side-menu">
|
|
||||||
<SessionsMenu onMenuItemClick={this.setActiveTab} toggleRehydratePanel={this.toggleRehydratePanel} />
|
|
||||||
</div>
|
|
||||||
<div className={cn('side-menu-margined', stl.searchWrapper)}>
|
|
||||||
<NoSessionsMessage />
|
|
||||||
<div className="mb-5">
|
|
||||||
<MainSearchBar />
|
|
||||||
<SessionSearch />
|
|
||||||
</div>
|
|
||||||
<SessionList onMenuItemClick={this.setActiveTab} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { applyFilter } from 'Duck/search';
|
|
||||||
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
|
|
||||||
import DateRangeDropdown from 'Shared/DateRangeDropdown';
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
filter: state.getIn([ 'search', 'instance' ]),
|
|
||||||
}), {
|
|
||||||
applyFilter, fetchFunnelsList
|
|
||||||
})
|
|
||||||
export default class DateRange extends React.PureComponent {
|
|
||||||
onDateChange = (e) => {
|
|
||||||
// this.props.fetchFunnelsList(e.rangeValue)
|
|
||||||
this.props.applyFilter(e)
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
const { filter: { rangeValue, startDate, endDate }, className } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DateRangeDropdown
|
|
||||||
button
|
|
||||||
onChange={ this.onDateChange }
|
|
||||||
rangeValue={ rangeValue }
|
|
||||||
startDate={ startDate }
|
|
||||||
endDate={ endDate }
|
|
||||||
className={ className }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Icon } from 'UI';
|
|
||||||
import stl from './filterSelectionButton.module.css';
|
|
||||||
|
|
||||||
const FilterSelectionButton = ({ label }) => {
|
|
||||||
return (
|
|
||||||
<div className={ stl.wrapper }>
|
|
||||||
<span className="capitalize">{ label } </span>
|
|
||||||
<Icon name="chevron-down"/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FilterSelectionButton;
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import Select from 'Shared/Select';
|
|
||||||
import { Icon } from 'UI';
|
|
||||||
import { sort } from 'Duck/sessions';
|
|
||||||
import { applyFilter } from 'Duck/search';
|
|
||||||
import stl from './sortDropdown.module.css';
|
|
||||||
|
|
||||||
@connect(null, { sort, applyFilter })
|
|
||||||
export default class SortDropdown extends React.PureComponent {
|
|
||||||
state = { value: null }
|
|
||||||
sort = ({ value }) => {
|
|
||||||
value = value.value
|
|
||||||
this.setState({ value: value })
|
|
||||||
const [ sort, order ] = value.split('-');
|
|
||||||
const sign = order === 'desc' ? -1 : 1;
|
|
||||||
this.props.applyFilter({ order, sort });
|
|
||||||
|
|
||||||
this.props.sort(sort, sign)
|
|
||||||
setTimeout(() => this.props.sort(sort, sign), 3000); //AAA
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { options } = this.props;
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
name="sortSessions"
|
|
||||||
plain
|
|
||||||
right
|
|
||||||
options={ options }
|
|
||||||
onChange={ this.sort }
|
|
||||||
defaultValue={ options[ 0 ].value }
|
|
||||||
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Filters';
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
.dropdown {
|
|
||||||
display: flex !important;
|
|
||||||
padding: 4px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: $gray-darkest;
|
|
||||||
font-weight: 500;
|
|
||||||
&:hover {
|
|
||||||
background-color: $gray-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownTrigger {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 3px;
|
|
||||||
&:hover {
|
|
||||||
background-color: $gray-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownIcon {
|
|
||||||
margin-top: 2px;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import styles from './insights.module.css';
|
|
||||||
|
|
||||||
const Insights = ({ insights }) => (
|
|
||||||
<div className={ styles.notes }>
|
|
||||||
<div className={ styles.tipText }>
|
|
||||||
<i className={ styles.tipIcon } />
|
|
||||||
{'This journey is only 2% of all the journeys but represents 20% of problems.'}
|
|
||||||
</div>
|
|
||||||
<div className={ styles.tipText }>
|
|
||||||
<i className={ styles.tipIcon } />
|
|
||||||
{'Lorem Ipsum 1290 events of 1500 events.'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
Insights.displayName = 'Insights';
|
|
||||||
|
|
||||||
export default connect(state => ({
|
|
||||||
insights: state.getIn([ 'sessions', 'insights' ]),
|
|
||||||
}))(Insights);
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import stl from './listHeader.module.css';
|
|
||||||
|
|
||||||
const ListHeader = ({ title }) => {
|
|
||||||
return (
|
|
||||||
<div className={ stl.header }>{ title }</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListHeader;
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Loader, NoContent, Pagination } from 'UI';
|
|
||||||
import { applyFilter, addAttribute, addEvent } from 'Duck/filters';
|
|
||||||
import { fetchSessions, addFilterByKeyAndValue, updateCurrentPage, setScrollPosition } from 'Duck/search';
|
|
||||||
import SessionItem from 'Shared/SessionItem';
|
|
||||||
import SessionListHeader from './SessionListHeader';
|
|
||||||
import { FilterKey } from 'Types/filter/filterType';
|
|
||||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
|
||||||
|
|
||||||
// const ALL = 'all';
|
|
||||||
const PER_PAGE = 10;
|
|
||||||
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
|
|
||||||
var timeoutId;
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
shouldAutorefresh: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 0,
|
|
||||||
savedFilters: state.getIn([ 'filters', 'list' ]),
|
|
||||||
loading: state.getIn([ 'sessions', 'loading' ]),
|
|
||||||
activeTab: state.getIn([ 'search', 'activeTab' ]),
|
|
||||||
allList: state.getIn([ 'sessions', 'list' ]),
|
|
||||||
total: state.getIn([ 'sessions', 'total' ]),
|
|
||||||
filters: state.getIn([ 'search', 'instance', 'filters' ]),
|
|
||||||
metaList: state.getIn(['customFields', 'list']).map(i => i.key),
|
|
||||||
currentPage: state.getIn([ 'search', 'currentPage' ]),
|
|
||||||
scrollY: state.getIn([ 'search', 'scrollY' ]),
|
|
||||||
lastPlayedSessionId: state.getIn([ 'sessions', 'lastPlayedSessionId' ]),
|
|
||||||
}), {
|
|
||||||
applyFilter,
|
|
||||||
addAttribute,
|
|
||||||
addEvent,
|
|
||||||
fetchSessions,
|
|
||||||
addFilterByKeyAndValue,
|
|
||||||
updateCurrentPage,
|
|
||||||
setScrollPosition,
|
|
||||||
})
|
|
||||||
export default class SessionList extends React.PureComponent {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.timeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
onUserClick = (userId, userAnonymousId) => {
|
|
||||||
if (userId) {
|
|
||||||
this.props.addFilterByKeyAndValue(FilterKey.USERID, userId);
|
|
||||||
} else {
|
|
||||||
this.props.addFilterByKeyAndValue(FilterKey.USERID, '', 'isUndefined');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout = () => {
|
|
||||||
timeoutId = setTimeout(function () {
|
|
||||||
if (this.props.shouldAutorefresh) {
|
|
||||||
// this.props.applyFilter();
|
|
||||||
this.props.fetchSessions();
|
|
||||||
}
|
|
||||||
this.timeout();
|
|
||||||
}.bind(this), AUTOREFRESH_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
getNoContentMessage = activeTab => {
|
|
||||||
let str = "No recordings found";
|
|
||||||
if (activeTab.type !== 'all') {
|
|
||||||
str += ' with ' + activeTab.name;
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
return str + '!';
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.setScrollPosition(window.scrollY)
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { scrollY } = this.props;
|
|
||||||
window.scrollTo(0, scrollY);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderActiveTabContent(list) {
|
|
||||||
const {
|
|
||||||
loading,
|
|
||||||
filters,
|
|
||||||
activeTab,
|
|
||||||
metaList,
|
|
||||||
currentPage,
|
|
||||||
total,
|
|
||||||
lastPlayedSessionId,
|
|
||||||
} = this.props;
|
|
||||||
const _filterKeys = filters.map(i => i.key);
|
|
||||||
const hasUserFilter = _filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white p-3 rounded border">
|
|
||||||
<NoContent
|
|
||||||
title={<div className="flex items-center justify-center flex-col">
|
|
||||||
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
|
|
||||||
{this.getNoContentMessage(activeTab)}
|
|
||||||
</div>}
|
|
||||||
// subtext="Please try changing your search parameters."
|
|
||||||
// animatedIcon="no-results"
|
|
||||||
show={ !loading && list.size === 0}
|
|
||||||
subtext={
|
|
||||||
<div>
|
|
||||||
<div>Please try changing your search parameters.</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
<Loader loading={ loading }>
|
|
||||||
{ list.map(session => (
|
|
||||||
<React.Fragment key={ session.sessionId }>
|
|
||||||
<SessionItem
|
|
||||||
session={ session }
|
|
||||||
hasUserFilter={hasUserFilter}
|
|
||||||
onUserClick={this.onUserClick}
|
|
||||||
metaList={metaList}
|
|
||||||
lastPlayedSessionId={lastPlayedSessionId}
|
|
||||||
/>
|
|
||||||
<div className="border-b" />
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</Loader>
|
|
||||||
<div className="w-full flex items-center justify-center py-6">
|
|
||||||
<Pagination
|
|
||||||
page={currentPage}
|
|
||||||
totalPages={Math.ceil(total / PER_PAGE)}
|
|
||||||
onPageChange={(page) => this.props.updateCurrentPage(page)}
|
|
||||||
limit={PER_PAGE}
|
|
||||||
debounceRequest={1000}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</NoContent>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { activeTab, allList, total } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="">
|
|
||||||
<SessionListHeader activeTab={activeTab} count={total}/>
|
|
||||||
{ this.renderActiveTabContent(allList) }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Button } from 'UI';
|
|
||||||
import styles from './sessionListFooter.module.css';
|
|
||||||
|
|
||||||
const SessionListFooter = ({
|
|
||||||
displayedCount, totalCount, loading, onLoadMoreClick,
|
|
||||||
}) => (
|
|
||||||
<div className={ styles.pageLoading }>
|
|
||||||
<div className={ styles.countInfo }>
|
|
||||||
{ `Displaying ${ displayedCount } of ${ totalCount }` }
|
|
||||||
</div>
|
|
||||||
{ totalCount > displayedCount &&
|
|
||||||
<Button
|
|
||||||
onClick={ onLoadMoreClick }
|
|
||||||
disabled={ loading }
|
|
||||||
loading={ loading }
|
|
||||||
outline
|
|
||||||
>
|
|
||||||
{ 'Load more...' }
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
SessionListFooter.displayName = 'SessionListFooter';
|
|
||||||
|
|
||||||
export default connect(state => ({
|
|
||||||
loading: state.getIn([ 'sessions', 'loading' ])
|
|
||||||
}))(SessionListFooter);
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import SortDropdown from '../Filters/SortDropdown';
|
|
||||||
import { numberWithCommas } from 'App/utils';
|
|
||||||
import SelectDateRange from 'Shared/SelectDateRange';
|
|
||||||
import { applyFilter } from 'Duck/search';
|
|
||||||
import Record from 'Types/app/period';
|
|
||||||
import { useStore } from 'App/mstore';
|
|
||||||
import { useObserver } from 'mobx-react-lite';
|
|
||||||
import { moment } from 'App/dateRange';
|
|
||||||
|
|
||||||
const sortOptionsMap = {
|
|
||||||
'startTs-desc': 'Newest',
|
|
||||||
'startTs-asc': 'Oldest',
|
|
||||||
'eventsCount-asc': 'Events Ascending',
|
|
||||||
'eventsCount-desc': 'Events Descending',
|
|
||||||
};
|
|
||||||
const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({ value, label }));
|
|
||||||
|
|
||||||
function SessionListHeader({ activeTab, count, applyFilter, filter }) {
|
|
||||||
const { settingsStore } = useStore();
|
|
||||||
|
|
||||||
const label = useObserver(() => settingsStore.sessionSettings.timezone.label);
|
|
||||||
const getTimeZoneOffset = React.useCallback(() => {
|
|
||||||
return label.slice(-6);
|
|
||||||
}, [label]);
|
|
||||||
|
|
||||||
const { startDate, endDate, rangeValue } = filter;
|
|
||||||
const period = new Record({ start: startDate, end: endDate, rangeName: rangeValue, timezoneOffset: getTimeZoneOffset() });
|
|
||||||
|
|
||||||
const onDateChange = (e) => {
|
|
||||||
const dateValues = e.toJSON();
|
|
||||||
dateValues.startDate = moment(dateValues.startDate).utcOffset(getTimeZoneOffset(), true).valueOf();
|
|
||||||
dateValues.endDate = moment(dateValues.endDate).utcOffset(getTimeZoneOffset(), true).valueOf();
|
|
||||||
applyFilter(dateValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (label) {
|
|
||||||
const dateValues = period.toJSON();
|
|
||||||
dateValues.startDate = moment(dateValues.startDate).startOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
|
|
||||||
dateValues.endDate = moment(dateValues.endDate).endOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
|
|
||||||
// applyFilter(dateValues);
|
|
||||||
}
|
|
||||||
}, [label]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex mb-2 justify-between items-end">
|
|
||||||
<div className="flex items-baseline">
|
|
||||||
<h3 className="text-2xl capitalize">
|
|
||||||
<span>{activeTab.name}</span>
|
|
||||||
<span className="ml-2 font-normal color-gray-medium">{count ? numberWithCommas(count) : 0}</span>
|
|
||||||
</h3>
|
|
||||||
{
|
|
||||||
<div className="ml-3 flex items-center">
|
|
||||||
<span className="mr-2 color-gray-medium">Sessions Captured in</span>
|
|
||||||
<SelectDateRange period={period} onChange={onDateChange} timezone={getTimeZoneOffset()} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="flex items-center ml-6">
|
|
||||||
<span className="mr-2 color-gray-medium">Sort By</span>
|
|
||||||
<SortDropdown options={sortOptions} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
(state) => ({
|
|
||||||
activeTab: state.getIn(['search', 'activeTab']),
|
|
||||||
period: state.getIn(['search', 'period']),
|
|
||||||
filter: state.getIn(['search', 'instance']),
|
|
||||||
}),
|
|
||||||
{ applyFilter }
|
|
||||||
)(SessionListHeader);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './SessionList';
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
.customMessage {
|
|
||||||
padding: 5px 10px !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
font-size: 12px !important;
|
|
||||||
color: $gray-medium !important;
|
|
||||||
font-weight: 300;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
& > div {
|
|
||||||
flex: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
@import 'mixins.css';
|
|
||||||
|
|
||||||
.pageLoading {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
align-items: center;
|
|
||||||
margin: 20px 0 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadMoreButton {
|
|
||||||
@mixin basicButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countInfo {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #999;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { SideMenuitem, Tooltip } from 'UI'
|
|
||||||
import stl from './sessionMenu.module.css';
|
|
||||||
import { clearEvents } from 'Duck/filters';
|
|
||||||
import { issues_types } from 'Types/session/issue'
|
|
||||||
import { fetchList as fetchSessionList } from 'Duck/sessions';
|
|
||||||
import { useModal } from 'App/components/Modal';
|
|
||||||
import SessionSettings from 'Shared/SessionSettings/SessionSettings'
|
|
||||||
|
|
||||||
function SessionsMenu(props) {
|
|
||||||
const { activeTab, isEnterprise } = props;
|
|
||||||
const { showModal } = useModal();
|
|
||||||
|
|
||||||
const onMenuItemClick = (filter) => {
|
|
||||||
props.onMenuItemClick(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={stl.wrapper}>
|
|
||||||
<div className={ cn(stl.header, 'flex items-center') }>
|
|
||||||
<div className={ stl.label }>
|
|
||||||
<span>Sessions</span>
|
|
||||||
</div>
|
|
||||||
<span className={ cn(stl.manageButton, 'mr-2') } onClick={() => showModal(<SessionSettings />, { right: true })}>
|
|
||||||
<Tooltip
|
|
||||||
title={<span>Configure the percentage of sessions <br /> to be captured, timezone and more.</span>}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<SideMenuitem
|
|
||||||
active={activeTab.type === 'all'}
|
|
||||||
title="All"
|
|
||||||
iconName="play-circle"
|
|
||||||
onClick={() => onMenuItemClick({ name: 'All', type: 'all' })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ issues_types.filter(item => item.visible).map(item => (
|
|
||||||
<SideMenuitem
|
|
||||||
key={item.key}
|
|
||||||
active={activeTab.type === item.type}
|
|
||||||
title={item.name} iconName={item.icon}
|
|
||||||
onClick={() => onMenuItemClick(item)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className={cn(stl.divider, 'my-4')} />
|
|
||||||
<SideMenuitem
|
|
||||||
title={ isEnterprise ? "Vault" : "Bookmarks" }
|
|
||||||
iconName={ isEnterprise ? "safe" : "star" }
|
|
||||||
active={activeTab.type === 'bookmark'}
|
|
||||||
onClick={() => onMenuItemClick({ name: isEnterprise ? 'Vault' : 'Bookmarks', type: 'bookmark', description: isEnterprise ? 'Sessions saved to vault never get\'s deleted from records.' : '' })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(state => ({
|
|
||||||
activeTab: state.getIn([ 'search', 'activeTab' ]),
|
|
||||||
captureRate: state.getIn(['watchdogs', 'captureRate']),
|
|
||||||
filters: state.getIn([ 'filters', 'appliedFilter' ]),
|
|
||||||
sessionsLoading: state.getIn([ 'sessions', 'fetchLiveListRequest', 'loading' ]),
|
|
||||||
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
|
|
||||||
}), {
|
|
||||||
clearEvents, fetchSessionList
|
|
||||||
})(SessionsMenu);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './SessionsMenu';
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
.header {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
& .label {
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: gray;
|
|
||||||
letter-spacing: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .manageButton {
|
|
||||||
margin-left: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: $teal;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px 5px;
|
|
||||||
border: solid thin transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-bottom: -3px;
|
|
||||||
&:hover {
|
|
||||||
background-color: $gray-light;
|
|
||||||
color: $gray-darkest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
background-color: $gray-light;
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { Icon } from 'UI';
|
|
||||||
import stl from './tabItem.module.css';
|
|
||||||
|
|
||||||
const TabItem = ({ icon, label, count, iconColor = 'teal', active = false, leading, ...rest }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
count === 0 ? stl.disabled : '',
|
|
||||||
cn(stl.wrapper,
|
|
||||||
active ? stl.active : '',
|
|
||||||
"flex items-center py-2 justify-between")
|
|
||||||
}
|
|
||||||
{ ...rest }
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
{ icon && <Icon name={ icon } size="16" color={ iconColor } /> }
|
|
||||||
<span className="ml-3 mr-1">{ label }</span>
|
|
||||||
{ count && <span>({ count })</span>}
|
|
||||||
</div>
|
|
||||||
{ !!leading && leading }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TabItem;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './TabItem'
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
.wrapper {
|
|
||||||
color: $teal;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 5px;
|
|
||||||
border: solid thin transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-left: -5px;
|
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:hover {
|
|
||||||
background-color: $active-blue;
|
|
||||||
border-color: $active-blue-border;
|
|
||||||
& .actionWrapper {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
@import 'mixins.css';
|
|
||||||
|
|
||||||
.searchWrapper {
|
|
||||||
flex: 1;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.bottom {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-top: solid thin #EDEDED;
|
|
||||||
& > div {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 10px;
|
|
||||||
border-right: solid thin $gray-light;
|
|
||||||
&:hover {
|
|
||||||
background-color: $active-blue;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
border-right: solid thin transparent;
|
|
||||||
}
|
|
||||||
&:first-child {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.savedSearchesWrapper {
|
|
||||||
width: 200px;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.header {
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
color: $gray-medium;
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import { storiesOf } from '@storybook/react';
|
|
||||||
import SessionsMenu from './SessionsMenu/SessionsMenu';
|
|
||||||
import SessionItem from 'Shared/SessionItem';
|
|
||||||
import SessionStack from 'Shared/SessionStack';
|
|
||||||
import Session from 'Types/session';
|
|
||||||
import SessionListHeader from './SessionList/SessionListHeader';
|
|
||||||
import SavedFilter from 'Types/filter/savedFilter';
|
|
||||||
import { List } from 'immutable';
|
|
||||||
|
|
||||||
var items = [
|
|
||||||
{
|
|
||||||
"watchdogId": 140,
|
|
||||||
"projectId": 1,
|
|
||||||
"type": "errors",
|
|
||||||
"payload": {
|
|
||||||
"threshold": 0,
|
|
||||||
"captureAll": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"watchdogId": 139,
|
|
||||||
"projectId": 1,
|
|
||||||
"type": "bad_request",
|
|
||||||
"payload": {
|
|
||||||
"threshold": 0,
|
|
||||||
"captureAll": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
var session = Session({
|
|
||||||
"projectId": 1,
|
|
||||||
"sessionId": "2236890417118217",
|
|
||||||
"userUuid": "1e4bec88-fe8d-4f51-9806-716e92384ffc",
|
|
||||||
"userId": null,
|
|
||||||
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
|
|
||||||
"userOs": "Mac OS X",
|
|
||||||
"userBrowser": "Chrome",
|
|
||||||
"userDevice": "Mac",
|
|
||||||
"userCountry": "FR",
|
|
||||||
"startTs": 1584132239030,
|
|
||||||
"duration": 618469,
|
|
||||||
"eventsCount": 24,
|
|
||||||
"pagesCount": 18,
|
|
||||||
"errorsCount": 0,
|
|
||||||
"watchdogs": [
|
|
||||||
137,
|
|
||||||
143
|
|
||||||
],
|
|
||||||
"favorite": false,
|
|
||||||
"viewed": false
|
|
||||||
})
|
|
||||||
|
|
||||||
var savedFilters = [
|
|
||||||
SavedFilter({filterId: 1, name: 'Something', count: 10, watchdogs: []})
|
|
||||||
]
|
|
||||||
|
|
||||||
storiesOf('Bug Finder', module)
|
|
||||||
.add('Sessions Menu', () => (
|
|
||||||
<SessionsMenu items={ items } />
|
|
||||||
))
|
|
||||||
.add('Sessions Item', () => (
|
|
||||||
<SessionItem key={1} session={session}/>
|
|
||||||
))
|
|
||||||
.add('Session List Header', () => (
|
|
||||||
<SessionListHeader />
|
|
||||||
))
|
|
||||||
.add('Sessions Stack', () => (
|
|
||||||
<SessionStack flow={savedFilters[0]} />
|
|
||||||
))
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 28px;
|
|
||||||
border: solid thin rgba(34, 36, 38, 0.15) !important;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0 10px;
|
|
||||||
width: 150px;
|
|
||||||
color: $gray-darkest;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
|
||||||
&:hover {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
& span {
|
|
||||||
margin-right: 5px;
|
|
||||||
max-width: 140px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './BugFinder';
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
|
|
||||||
@import 'icons.css';
|
|
||||||
|
|
||||||
.notes {
|
|
||||||
margin: 15px 0;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
.tipIcon {
|
|
||||||
@mixin icon lightbulb, $gray-medium, 13px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tipText {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: $gray-medium;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
.header {
|
|
||||||
padding: 3px 10px;
|
|
||||||
letter-spacing: 1.5px;
|
|
||||||
color: $gray-medium;
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
@ -4,8 +4,7 @@ import { Set, List as ImmutableList } from "immutable";
|
||||||
import { NoContent, Loader, Checkbox, LoadMoreButton, IconButton, Input, DropdownPlain, Pagination } from 'UI';
|
import { NoContent, Loader, Checkbox, LoadMoreButton, IconButton, Input, DropdownPlain, Pagination } from 'UI';
|
||||||
import { merge, resolve, unresolve, ignore, updateCurrentPage, editOptions } from "Duck/errors";
|
import { merge, resolve, unresolve, ignore, updateCurrentPage, editOptions } from "Duck/errors";
|
||||||
import { applyFilter } from 'Duck/filters';
|
import { applyFilter } from 'Duck/filters';
|
||||||
import { IGNORED, RESOLVED, UNRESOLVED } from 'Types/errorInfo';
|
import { IGNORED, UNRESOLVED } from 'Types/errorInfo';
|
||||||
import SortDropdown from 'Components/BugFinder/Filters/SortDropdown';
|
|
||||||
import Divider from 'Components/Errors/ui/Divider';
|
import Divider from 'Components/Errors/ui/Divider';
|
||||||
import ListItem from './ListItem/ListItem';
|
import ListItem from './ListItem/ListItem';
|
||||||
import { debounce } from 'App/utils';
|
import { debounce } from 'App/utils';
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import stl from './sessionStack.module.css'
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { Icon } from 'UI'
|
|
||||||
import { names } from 'Types/watchdog'
|
|
||||||
import { applySavedFilter, setActiveFlow } from 'Duck/filters';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { setActiveTab } from 'Duck/sessions';
|
|
||||||
|
|
||||||
const IconLabel = ({ icon, label}) => (
|
|
||||||
<div className="w-9/12 flex items-center justify-end">
|
|
||||||
<Icon name={icon} size="20" color={label > 0 ? 'gray' : 'gray-medium'} />
|
|
||||||
<div className={cn('ml-2 text-xl', label > 0 ? 'color-gray' : 'color-gray-medium')}>{label}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
function SessionStack({ flow = {}, applySavedFilter, setActiveTab, setActiveFlow }) {
|
|
||||||
const onAllClick = (flow) => {
|
|
||||||
setActiveFlow(flow)
|
|
||||||
applySavedFilter(flow.filter)
|
|
||||||
setActiveTab({ type: 'all', name: 'All'})
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={stl.wrapper}>
|
|
||||||
<div
|
|
||||||
className="text-xl mb-6 capitalize color-teal cursor-pointer"
|
|
||||||
onClick={() => onAllClick(flow)}>
|
|
||||||
{flow.name}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="w-2/12 text-xl"><span className="text-3xl">{flow.count}</span> Sessions</div>
|
|
||||||
<div className="w-6/12 flex items-center ml-auto">
|
|
||||||
{flow.watchdogs.map(({type, count}) => (
|
|
||||||
<IconLabel key={type} icon={names[type].icon} label={count} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, { applySavedFilter, setActiveTab, setActiveFlow })(SessionStack)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './SessionStack';
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
|
|
||||||
@import 'mixins.css';
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
background: #fff;
|
|
||||||
border: solid thin $gray-light;
|
|
||||||
border-radius: 3px;
|
|
||||||
@mixin defaultHover;
|
|
||||||
box-shadow:
|
|
||||||
/* The top layer shadow */
|
|
||||||
/* 0 1px 1px rgba(0,0,0,0.15), */
|
|
||||||
/* The second layer */
|
|
||||||
4px 4px 1px 1px white,
|
|
||||||
/* The second layer shadow */
|
|
||||||
4px 4px 0px 1px rgba(0,0,0,0.4);
|
|
||||||
/* Padding for demo purposes */
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,6 @@ export default Record({
|
||||||
filter: Filter(),
|
filter: Filter(),
|
||||||
createdAt: undefined,
|
createdAt: undefined,
|
||||||
count: 0,
|
count: 0,
|
||||||
watchdogs: List(),
|
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
}, {
|
}, {
|
||||||
idKey: 'searchId',
|
idKey: 'searchId',
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ export default Record(
|
||||||
returningLocation: undefined,
|
returningLocation: undefined,
|
||||||
returningLocationTime: undefined,
|
returningLocationTime: undefined,
|
||||||
errorsCount: 0,
|
errorsCount: 0,
|
||||||
watchdogs: [],
|
|
||||||
issueTypes: [],
|
issueTypes: [],
|
||||||
issues: [],
|
issues: [],
|
||||||
userDeviceHeapSize: 0,
|
userDeviceHeapSize: 0,
|
||||||
|
|
@ -145,7 +144,6 @@ export default Record(
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
isIOS: session.platform === 'ios',
|
isIOS: session.platform === 'ios',
|
||||||
watchdogs: session.watchdogs || [],
|
|
||||||
errors: exceptions,
|
errors: exceptions,
|
||||||
siteId: projectId,
|
siteId: projectId,
|
||||||
events,
|
events,
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,4 @@ export interface SavedSearch {
|
||||||
projectId: number;
|
projectId: number;
|
||||||
searchId: number;
|
searchId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
watchdogs: List<any>
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue