Webpack upgrade and dependency cleanup (#523)
* change(ui) - webpack update * change(ui) - api optimize and other fixes
This commit is contained in:
parent
f5e013329f
commit
2ed5cac986
993 changed files with 12905 additions and 38918 deletions
14
frontend/.babelrc
Normal file
14
frontend/.babelrc
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
[ "@babel/plugin-proposal-private-property-in-object", { "loose": true } ],
|
||||
[ "@babel/plugin-transform-runtime", { "regenerator": true } ],
|
||||
[ "@babel/plugin-proposal-decorators", { "legacy":true } ],
|
||||
[ "@babel/plugin-proposal-class-properties", { "loose":true } ],
|
||||
[ "@babel/plugin-proposal-private-methods", { "loose": true }]
|
||||
]
|
||||
}
|
||||
26
frontend/.env.sample
Normal file
26
frontend/.env.sample
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
NODE_ENV=production
|
||||
SOURCEMAP = false
|
||||
|
||||
# END POINTS #
|
||||
ORIGIN = ''
|
||||
API_EDP = ''
|
||||
ASSETS_HOST = ''
|
||||
|
||||
# SENTRY
|
||||
SENTRY_ENABLED = false
|
||||
SENTRY_URL = ''
|
||||
|
||||
# CAPTCHA
|
||||
CAPTCHA_ENABLED = false
|
||||
CAPTCHA_SITE_KEY = 'asdad'
|
||||
|
||||
# MINIO
|
||||
MINIO_ENDPOINT = ''
|
||||
MINIO_PORT = ''
|
||||
MINIO_USE_SSL = ''
|
||||
MINIO_ACCESS_KEY = ''
|
||||
MINIO_SECRET_KEY = ''
|
||||
|
||||
# APP and TRACKER VERSIONS
|
||||
VERSION = '1.6.0'
|
||||
TRACKER_VERSION = '3.5.10'
|
||||
|
|
@ -21,12 +21,10 @@ const FunnelDetails = lazy(() => import('Components/Funnels/FunnelDetails'));
|
|||
const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDetails'));
|
||||
import WidgetViewPure from 'Components/Dashboard/components/WidgetView';
|
||||
import Header from 'Components/Header/Header';
|
||||
// import ResultsModal from 'Shared/Results/ResultsModal';
|
||||
import { fetchList as fetchMetadata } from 'Duck/customField';
|
||||
import { fetchList as fetchSiteList } from 'Duck/site';
|
||||
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
|
||||
import { fetchList as fetchAlerts } from 'Duck/alerts';
|
||||
import { dashboardService } from "App/services";
|
||||
import { withStore } from 'App/mstore'
|
||||
|
||||
import APIClient from './api_client';
|
||||
|
|
@ -36,7 +34,6 @@ import Signup from './components/Signup/Signup';
|
|||
import { fetchTenants } from 'Duck/user';
|
||||
import { setSessionPath } from 'Duck/sessions';
|
||||
import { ModalProvider } from './components/Modal';
|
||||
import ModalRoot from './components/Modal/ModalRoot';
|
||||
|
||||
const BugFinder = withSiteIdUpdater(BugFinderPure);
|
||||
const Dashboard = withSiteIdUpdater(DashboardPure);
|
||||
|
|
@ -50,7 +47,7 @@ const Errors = withSiteIdUpdater(ErrorsPure);
|
|||
const Funnels = withSiteIdUpdater(FunnelDetails);
|
||||
const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails);
|
||||
const withSiteId = routes.withSiteId;
|
||||
const withObTab = routes.withObTab;
|
||||
// const withObTab = routes.withObTab;
|
||||
|
||||
const METRICS_PATH = routes.metrics();
|
||||
const METRICS_DETAILS = routes.metricDetails();
|
||||
|
|
@ -115,8 +112,9 @@ class Router extends React.Component {
|
|||
super(props);
|
||||
if (props.isLoggedIn) {
|
||||
this.fetchInitialData();
|
||||
} else {
|
||||
props.fetchTenants();
|
||||
}
|
||||
props.fetchTenants();
|
||||
}
|
||||
|
||||
fetchInitialData = () => {
|
||||
|
|
@ -126,11 +124,11 @@ class Router extends React.Component {
|
|||
const { mstore } = this.props
|
||||
mstore.initClient();
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.fetchMetadata()
|
||||
this.props.fetchAnnouncements();
|
||||
this.props.fetchAlerts();
|
||||
}, 100);
|
||||
// setTimeout(() => {
|
||||
// this.props.fetchMetadata()
|
||||
// this.props.fetchAnnouncements();
|
||||
// this.props.fetchAlerts();
|
||||
// }, 100);
|
||||
})
|
||||
})
|
||||
])
|
||||
|
|
@ -166,12 +164,11 @@ class Router extends React.Component {
|
|||
|
||||
return isLoggedIn ?
|
||||
<Loader loading={ loading } className="flex-1" >
|
||||
{!hideHeader && <Header key="header"/>}
|
||||
<Notification />
|
||||
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
<ModalProvider>
|
||||
<ModalRoot />
|
||||
{!hideHeader && <Header key="header"/>}
|
||||
<Switch key="content" >
|
||||
<Route path={ CLIENT_PATH } component={ Client } />
|
||||
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
|
||||
|
|
|
|||
|
|
@ -82,8 +82,7 @@ export default class APIClient {
|
|||
|
||||
|
||||
let fetch = window.fetch;
|
||||
|
||||
let edp = window.ENV.API_EDP;
|
||||
let edp = window.env.API_EDP || window.location.origin + '/api';
|
||||
if (
|
||||
path !== '/targets_temp' &&
|
||||
!path.includes('/metadata/session_search') &&
|
||||
|
|
|
|||
BIN
frontend/app/assets/img/logo-open-replay-grey.png
Normal file
BIN
frontend/app/assets/img/logo-open-replay-grey.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
|
|
@ -5,10 +5,9 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="slack-app-id" content="AA5LEB34M">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,28 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { Button, Dropdown, Form, Input, SegmentSelection, Checkbox, Message, Link, Icon } from 'UI';
|
||||
import { Button, Form, Input, SegmentSelection, Checkbox, Message, Link, Icon } from 'UI';
|
||||
import { alertMetrics as metrics } from 'App/constants';
|
||||
import { alertConditions as conditions } from 'App/constants';
|
||||
import { client, CLIENT_TABS } from 'App/routes';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './alertForm.css';
|
||||
import stl from './alertForm.module.css';
|
||||
import DropdownChips from './DropdownChips';
|
||||
import { validateEmail } from 'App/validate';
|
||||
import cn from 'classnames';
|
||||
import { fetchTriggerOptions } from 'Duck/alerts';
|
||||
import Select from 'Shared/Select'
|
||||
|
||||
const thresholdOptions = [
|
||||
{ text: '15 minutes', value: 15 },
|
||||
{ text: '30 minutes', value: 30 },
|
||||
{ text: '1 hour', value: 60 },
|
||||
{ text: '2 hours', value: 120 },
|
||||
{ text: '4 hours', value: 240 },
|
||||
{ text: '1 day', value: 1440 },
|
||||
{ label: '15 minutes', value: 15 },
|
||||
{ label: '30 minutes', value: 30 },
|
||||
{ label: '1 hour', value: 60 },
|
||||
{ label: '2 hours', value: 120 },
|
||||
{ label: '4 hours', value: 240 },
|
||||
{ label: '1 day', value: 1440 },
|
||||
];
|
||||
|
||||
const changeOptions = [
|
||||
{ text: 'change', value: 'change' },
|
||||
{ text: '% change', value: 'percent' },
|
||||
{ label: 'change', value: 'change' },
|
||||
{ label: '% change', value: 'percent' },
|
||||
];
|
||||
|
||||
const Circle = ({ text }) => (
|
||||
|
|
@ -50,7 +51,9 @@ const AlertForm = props => {
|
|||
const { instance, slackChannels, webhooks, loading, onDelete, deleting, triggerOptions, metricId, style={ width: '580px', height: '100vh' } } = props;
|
||||
const write = ({ target: { value, name } }) => props.edit({ [ name ]: value })
|
||||
const writeOption = (e, { name, value }) => props.edit({ [ name ]: value });
|
||||
const onChangeOption = (e, { checked, name }) => props.edit({ [ name ]: checked })
|
||||
const onChangeCheck = ({ target: { checked, name }}) => props.edit({ [ name ]: checked })
|
||||
// const onChangeOption = ({ checked, name }) => props.edit({ [ name ]: checked })
|
||||
// const onChangeCheck = (e) => { console.log(e) }
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchTriggerOptions();
|
||||
|
|
@ -75,7 +78,7 @@ const AlertForm = props => {
|
|||
<div className={cn(stl.content, '-mx-6 px-6 pb-12')}>
|
||||
<input
|
||||
autoFocus={ true }
|
||||
className="text-lg"
|
||||
className="text-lg border border-gray-light rounded w-full"
|
||||
name="name"
|
||||
style={{ fontSize: '18px', padding: '10px', fontWeight: '600'}}
|
||||
value={ instance && instance.name }
|
||||
|
|
@ -119,14 +122,13 @@ const AlertForm = props => {
|
|||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
|
||||
<Dropdown
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="change"
|
||||
selection
|
||||
options={ changeOptions }
|
||||
name="change"
|
||||
value={ instance.change }
|
||||
onChange={ writeOption }
|
||||
defaultValue={ instance.change }
|
||||
onChange={ ({ value }) => writeOption(null , { name: 'change', value }) }
|
||||
id="change-dropdown"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -134,29 +136,28 @@ const AlertForm = props => {
|
|||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{isThreshold ? 'Trigger when' : 'of'}</label>
|
||||
<Dropdown
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="Select Metric"
|
||||
selection
|
||||
search
|
||||
isSearchable={true}
|
||||
options={ triggerOptions }
|
||||
name="left"
|
||||
value={ instance.query.left }
|
||||
onChange={ writeQueryOption }
|
||||
value={ triggerOptions.find(i => i.value === instance.query.left) }
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={ ({ value }) => writeQueryOption(null, { name: 'left', value }) }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'is'}</label>
|
||||
<div className="w-4/6 flex items-center">
|
||||
<Dropdown
|
||||
className="px-4"
|
||||
<Select
|
||||
placeholder="Select Condition"
|
||||
selection
|
||||
options={ conditions }
|
||||
name="operator"
|
||||
value={ instance.query.operator }
|
||||
onChange={ writeQueryOption }
|
||||
defaultValue={ instance.query.operator }
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={ ({ value }) => writeQueryOption(null, { name: 'operator', value }) }
|
||||
/>
|
||||
{ unit && (
|
||||
<Input
|
||||
|
|
@ -172,39 +173,40 @@ const AlertForm = props => {
|
|||
)}
|
||||
{ !unit && (
|
||||
<Input
|
||||
className="pl-4"
|
||||
name="right"
|
||||
value={ instance.query.right }
|
||||
onChange={ writeQuery }
|
||||
placeholder="Specify Value"
|
||||
/>
|
||||
wrapperClassName="ml-2"
|
||||
// className="pl-4"
|
||||
name="right"
|
||||
value={ instance.query.right }
|
||||
onChange={ writeQuery }
|
||||
placeholder="Specify Value"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'over the past'}</label>
|
||||
<Dropdown
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
selection
|
||||
options={ thresholdOptions }
|
||||
name="currentPeriod"
|
||||
value={ instance.currentPeriod }
|
||||
onChange={ writeOption }
|
||||
defaultValue={ instance.currentPeriod }
|
||||
// onChange={ writeOption }
|
||||
onChange={ ({ value }) => writeOption(null, { name: 'currentPeriod', value }) }
|
||||
/>
|
||||
</div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'compared to previous'}</label>
|
||||
<Dropdown
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
selection
|
||||
options={ thresholdOptions }
|
||||
name="previousPeriod"
|
||||
value={ instance.previousPeriod }
|
||||
onChange={ writeOption }
|
||||
defaultValue={ instance.previousPeriod }
|
||||
// onChange={ writeOption }
|
||||
onChange={ ({ value }) => writeOption(null, { name: 'previousPeriod', value }) }
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -223,18 +225,17 @@ const AlertForm = props => {
|
|||
<div className="flex items-center my-4">
|
||||
<Checkbox
|
||||
name="slack"
|
||||
className="font-medium"
|
||||
className="mr-8"
|
||||
type="checkbox"
|
||||
checked={ instance.slack }
|
||||
onClick={ onChangeOption }
|
||||
className="mr-8"
|
||||
onClick={ onChangeCheck }
|
||||
label="Slack"
|
||||
/>
|
||||
<Checkbox
|
||||
name="email"
|
||||
type="checkbox"
|
||||
checked={ instance.email }
|
||||
onClick={ onChangeOption }
|
||||
onClick={ onChangeCheck }
|
||||
className="mr-8"
|
||||
label="Email"
|
||||
/>
|
||||
|
|
@ -242,7 +243,7 @@ const AlertForm = props => {
|
|||
name="webhook"
|
||||
type="checkbox"
|
||||
checked={ instance.webhook }
|
||||
onClick={ onChangeOption }
|
||||
onClick={ onChangeCheck }
|
||||
label="Webhook"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -300,7 +301,7 @@ const AlertForm = props => {
|
|||
<div className="flex items-center">
|
||||
<Button
|
||||
loading={loading}
|
||||
primary
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={loading || !instance.validate()}
|
||||
id="submit-button"
|
||||
|
|
@ -314,9 +315,9 @@ const AlertForm = props => {
|
|||
{instance.exists() && (
|
||||
<Button
|
||||
hover
|
||||
variant="text"
|
||||
loading={deleting}
|
||||
type="button"
|
||||
outline plain
|
||||
onClick={() => onDelete(instance)}
|
||||
id="trash-button"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import AlertForm from '../AlertForm';
|
|||
import { connect } from 'react-redux';
|
||||
import { setShowAlerts } from 'Duck/dashboard';
|
||||
import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
import { confirm } from 'UI';
|
||||
|
||||
interface Props {
|
||||
showModal?: boolean;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames';
|
||||
import stl from './alertItem.css';
|
||||
import stl from './alertItem.module.css';
|
||||
import AlertTypeLabel from './AlertTypeLabel';
|
||||
|
||||
const AlertItem = props => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import stl from './alertTypeLabel.css'
|
||||
import stl from './alertTypeLabel.module.css'
|
||||
|
||||
function AlertTypeLabel({ filterKey, type = '' }) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import AlertForm from './AlertForm';
|
|||
import { connect } from 'react-redux';
|
||||
import { setShowAlerts } from 'Duck/dashboard';
|
||||
import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
import { confirm } from 'UI';
|
||||
|
||||
const Alerts = props => {
|
||||
const { webhooks, setShowAlerts } = props;
|
||||
|
|
@ -18,8 +18,8 @@ const Alerts = props => {
|
|||
props.fetchWebhooks();
|
||||
}, [])
|
||||
|
||||
const slackChannels = webhooks.filter(hook => hook.type === SLACK).map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS();
|
||||
const hooks = webhooks.filter(hook => hook.type === WEBHOOK).map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS();
|
||||
const slackChannels = webhooks.filter(hook => hook.type === SLACK).map(({ webhookId, name }) => ({ value: webhookId, label: name })).toJS();
|
||||
const hooks = webhooks.filter(hook => hook.type === WEBHOOK).map(({ webhookId, name }) => ({ value: webhookId, label: name })).toJS();
|
||||
|
||||
const saveAlert = instance => {
|
||||
const wasUpdating = instance.exists();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import { Dropdown, TagBadge } from 'UI';
|
||||
import { Input, TagBadge } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
const DropdownChips = ({
|
||||
textFiled = false,
|
||||
|
|
@ -15,7 +16,7 @@ const DropdownChips = ({
|
|||
onChange(selected.filter(i => i !== id))
|
||||
}
|
||||
|
||||
const onSelect = (e, { name, value }) => {
|
||||
const onSelect = ({ value }) => {
|
||||
const newSlected = selected.concat(value);
|
||||
onChange(newSlected)
|
||||
};
|
||||
|
|
@ -23,20 +24,20 @@ const DropdownChips = ({
|
|||
const onKeyPress = e => {
|
||||
const val = e.target.value;
|
||||
if (e.key !== 'Enter' || selected.includes(val)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (validate && !validate(val)) return;
|
||||
|
||||
const newSlected = selected.concat(val);
|
||||
e.target.value = '';
|
||||
onChange(newSlected);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const _options = options.filter(item => !selected.includes(item.value))
|
||||
|
||||
const renderBadge = item => {
|
||||
const val = typeof item === 'string' ? item : item.value;
|
||||
const text = typeof item === 'string' ? item : item.text;
|
||||
const text = typeof item === 'string' ? item : item.label;
|
||||
return (
|
||||
<TagBadge
|
||||
className={badgeClassName}
|
||||
|
|
@ -52,15 +53,14 @@ const DropdownChips = ({
|
|||
return (
|
||||
<div className="w-full">
|
||||
{textFiled ? (
|
||||
<input type="text" onKeyPress={onKeyPress} placeholder={placeholder} />
|
||||
<Input type="text" onKeyPress={onKeyPress} placeholder={placeholder} />
|
||||
) : (
|
||||
<Dropdown
|
||||
<Select
|
||||
placeholder={placeholder}
|
||||
search
|
||||
selection
|
||||
isSearchable={true}
|
||||
options={ _options }
|
||||
name="webhookInput"
|
||||
value={ '' }
|
||||
value={null}
|
||||
onChange={ onSelect }
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'UI';
|
||||
import stl from './listItem.css';
|
||||
import stl from './listItem.module.css';
|
||||
import cn from 'classnames';
|
||||
import AlertTypeLabel from '../../AlertTypeLabel';
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ const ListItem = ({ alert, onClear, loading, onNavigate }) => {
|
|||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm">{alert.createdAt && alert.createdAt.toFormat('LLL dd, yyyy, hh:mm a')}</div>
|
||||
<div className={ cn("invisible", { 'group-hover:visible' : !alert.viewed})} >
|
||||
<Button plain simple loading={loading} noPadding>
|
||||
<Button variant="text" loading={loading}>
|
||||
<span className={ cn("text-sm color-gray-medium", { 'invisible' : loading })} onClick={onClear}>{'IGNORE'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
import React from 'react';
|
||||
import stl from './notifications.css';
|
||||
import ListItem from './ListItem';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, SlideModal, Icon, Popup, NoContent, SegmentSelection } from 'UI';
|
||||
import { fetchList, setViewed, setLastRead, clearAll } from 'Duck/notifications';
|
||||
import withToggle from 'Components/hocs/withToggle';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { fetchList as fetchAlerts, init as initAlert } from 'Duck/alerts';
|
||||
import cn from 'classnames';
|
||||
|
||||
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
@withToggle('visible', 'toggleVisisble')
|
||||
@withRouter
|
||||
class Notifications extends React.Component {
|
||||
state = { alertType: '' };
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// setTimeout(() => {
|
||||
// props.fetchList();
|
||||
// }, 1000);
|
||||
|
||||
setInterval(() => {
|
||||
props.fetchList();
|
||||
}, AUTOREFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
writeOption = (e, { name, value }) => this.setState({ [ name ]: value });
|
||||
|
||||
navigateToUrl = notification => { // TODO should be able to open the alert edit form
|
||||
if (notification.options.source === 'ALERT') {
|
||||
const { initAlert } = this.props;
|
||||
this.props.fetchAlerts().then(function() {
|
||||
const { alerts } = this.props;
|
||||
const alert = alerts.find(i => i.alertId === notification.options.sourceId)
|
||||
initAlert(alert.toJS());
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
onClearAll = () => {
|
||||
const { notifications } = this.props;
|
||||
const firstItem = notifications.first();
|
||||
this.props.clearAll({ endTimestamp: firstItem.createdAt.ts });
|
||||
}
|
||||
|
||||
onClear = notification => {
|
||||
this.props.setViewed(notification.notificationId)
|
||||
}
|
||||
|
||||
toggleModal = () => {
|
||||
this.props.toggleVisisble(!this.props.visible);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { notifications, visible, loading, clearing, clearingAll } = this.props;
|
||||
const { alertType } = this.state;
|
||||
const unReadNotificationsCount = notifications.filter(({viewed}) => !viewed).size
|
||||
|
||||
const filteredList = alertType === '' ?
|
||||
notifications :
|
||||
notifications.filter(i => i.filterKey === alertType);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popup
|
||||
trigger={
|
||||
<div className={ stl.button } onClick={ this.toggleModal } data-active={ visible }>
|
||||
<div className={ stl.counter } data-hidden={ unReadNotificationsCount === 0 }>
|
||||
{ unReadNotificationsCount }
|
||||
</div>
|
||||
<Icon name="bell" size="18" />
|
||||
</div>
|
||||
}
|
||||
content={ `Alerts` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
/>
|
||||
<SlideModal
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
<div>Alerts</div>
|
||||
{ unReadNotificationsCount > 0 && (
|
||||
<div className="">
|
||||
<Button
|
||||
loading={clearingAll}
|
||||
plain
|
||||
simple
|
||||
onClick={this.props.setLastRead}
|
||||
disabled={unReadNotificationsCount === 0}
|
||||
noPadding
|
||||
>
|
||||
<span
|
||||
className={ cn("text-sm color-gray-medium", { 'invisible' : clearingAll })}
|
||||
onClick={this.onClearAll}>
|
||||
IGNORE ALL
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
right
|
||||
isDisplayed={ visible }
|
||||
onClose={ visible && this.toggleModal }
|
||||
bgColor="white"
|
||||
size="small"
|
||||
content={
|
||||
<div className="">
|
||||
<NoContent
|
||||
title=""
|
||||
subtext="There are no alerts to show."
|
||||
animatedIcon="no-results"
|
||||
show={ !loading && notifications.size === 0 }
|
||||
size="small"
|
||||
>
|
||||
{
|
||||
filteredList.map(item => (
|
||||
<div className="border-b" key={item.key}>
|
||||
<ListItem
|
||||
key={item.key}
|
||||
alert={item}
|
||||
onClear={() => this.onClear(item)}
|
||||
loading={clearing}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</NoContent>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
notifications: state.getIn(['notifications', 'list']),
|
||||
loading: state.getIn(['notifications', 'fetchRequest', 'loading']),
|
||||
clearing: state.getIn(['notifications', 'setViewed', 'loading']),
|
||||
clearingAll: state.getIn(['notifications', 'clearAll', 'loading']),
|
||||
alerts: state.getIn(['alerts', 'list']),
|
||||
}), { fetchList, setLastRead, setViewed, clearAll, fetchAlerts, initAlert })(Notifications);
|
||||
184
frontend/app/components/Alerts/Notifications/Notifications.tsx
Normal file
184
frontend/app/components/Alerts/Notifications/Notifications.tsx
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import stl from './notifications.module.css';
|
||||
import ListItem from './ListItem';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, SlideModal, Icon, Popup, NoContent } from 'UI';
|
||||
import { fetchList, setViewed, clearAll } from 'Duck/notifications';
|
||||
import { setLastRead } from 'Duck/announcements';
|
||||
import cn from 'classnames';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import AlertTriggersModal from 'Shared/AlertTriggersModal';
|
||||
|
||||
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
// @withToggle('visible', 'toggleVisisble')
|
||||
// @withRouter
|
||||
// class Notifications extends React.Component {
|
||||
// state = { alertType: '' };
|
||||
|
||||
// constructor(props) {
|
||||
// super(props);
|
||||
// // setTimeout(() => {
|
||||
// // props.fetchList();
|
||||
// // }, 1000);
|
||||
|
||||
// setInterval(() => {
|
||||
// props.fetchList();
|
||||
// }, AUTOREFRESH_INTERVAL);
|
||||
// }
|
||||
|
||||
// writeOption = (e, { name, value }) => this.setState({ [ name ]: value });
|
||||
|
||||
// navigateToUrl = notification => { // TODO should be able to open the alert edit form
|
||||
// if (notification.options.source === 'ALERT') {
|
||||
// const { initAlert } = this.props;
|
||||
// this.props.fetchAlerts().then(function() {
|
||||
// const { alerts } = this.props;
|
||||
// const alert = alerts.find(i => i.alertId === notification.options.sourceId)
|
||||
// initAlert(alert.toJS());
|
||||
// }.bind(this));
|
||||
// }
|
||||
// }
|
||||
|
||||
// onClearAll = () => {
|
||||
// const { notifications } = this.props;
|
||||
// const firstItem = notifications.first();
|
||||
// this.props.clearAll({ endTimestamp: firstItem.createdAt.ts });
|
||||
// }
|
||||
|
||||
// onClear = notification => {
|
||||
// this.props.setViewed(notification.notificationId)
|
||||
// }
|
||||
|
||||
// toggleModal = () => {
|
||||
// this.props.toggleVisisble(!this.props.visible);
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// const { notifications, visible, loading, clearing, clearingAll } = this.props;
|
||||
// const { alertType } = this.state;
|
||||
// const unReadNotificationsCount = notifications.filter(({viewed}) => !viewed).size
|
||||
|
||||
// const filteredList = alertType === '' ?
|
||||
// notifications :
|
||||
// notifications.filter(i => i.filterKey === alertType);
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <Popup
|
||||
// content={ `Alerts` }
|
||||
// >
|
||||
// <div className={ stl.button } onClick={ this.toggleModal } data-active={ visible }>
|
||||
// <div className={ stl.counter } data-hidden={ unReadNotificationsCount === 0 }>
|
||||
// { unReadNotificationsCount }
|
||||
// </div>
|
||||
// <Icon name="bell" size="18" />
|
||||
// </div>
|
||||
// </Popup>
|
||||
// <SlideModal
|
||||
// title={
|
||||
// <div className="flex items-center justify-between">
|
||||
// <div>Alerts</div>
|
||||
// { unReadNotificationsCount > 0 && (
|
||||
// <div className="">
|
||||
// <Button
|
||||
// loading={clearingAll}
|
||||
// variant="text"
|
||||
// onClick={this.props.setLastRead}
|
||||
// disabled={unReadNotificationsCount === 0}
|
||||
// >
|
||||
// <span
|
||||
// className={ cn("text-sm color-gray-medium", { 'invisible' : clearingAll })}
|
||||
// onClick={this.onClearAll}>
|
||||
// IGNORE ALL
|
||||
// </span>
|
||||
// </Button>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// }
|
||||
// right
|
||||
// isDisplayed={ visible }
|
||||
// onClose={ visible && this.toggleModal }
|
||||
// bgColor="white"
|
||||
// size="small"
|
||||
// content={
|
||||
// <div className="">
|
||||
// <NoContent
|
||||
// title={
|
||||
// <div className="flex items-center justify-between">
|
||||
// <AnimatedSVG name={ICONS.EMPTY_STATE} size="100" />
|
||||
// </div>
|
||||
// }
|
||||
// subtext="There are no alerts to show."
|
||||
// show={ !loading && notifications.size === 0 }
|
||||
// size="small"
|
||||
// >
|
||||
// {
|
||||
// filteredList.map(item => (
|
||||
// <div className="border-b" key={item.key}>
|
||||
// <ListItem
|
||||
// key={item.key}
|
||||
// alert={item}
|
||||
// onClear={() => this.onClear(item)}
|
||||
// loading={clearing}
|
||||
// />
|
||||
// </div>
|
||||
// ))
|
||||
// }
|
||||
// </NoContent>
|
||||
// </div>
|
||||
// }
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// export default connect(state => ({
|
||||
// notifications: state.getIn(['notifications', 'list']),
|
||||
// loading: state.getIn(['notifications', 'fetchRequest', 'loading']),
|
||||
// clearing: state.getIn(['notifications', 'setViewed', 'loading']),
|
||||
// clearingAll: state.getIn(['notifications', 'clearAll', 'loading']),
|
||||
// alerts: state.getIn(['alerts', 'list']),
|
||||
// }), { fetchList, setLastRead, setViewed, clearAll, fetchAlerts, initAlert })(Notifications);
|
||||
|
||||
interface Props {
|
||||
notifications: any;
|
||||
fetchList: any;
|
||||
}
|
||||
function Notifications(props: Props) {
|
||||
const { notifications } = props;
|
||||
const { showModal } = useModal();
|
||||
const unReadNotificationsCount = notifications.filter(({viewed}: any) => !viewed).size
|
||||
|
||||
useEffect(() => {
|
||||
if (notifications.size === 0) {
|
||||
props.fetchList();
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
props.fetchList();
|
||||
}, AUTOREFRESH_INTERVAL);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
content={ `Alerts` }
|
||||
>
|
||||
<div className={ stl.button } onClick={ () => showModal(<AlertTriggersModal unReadNotificationsCount={unReadNotificationsCount} />, { right: true }) }>
|
||||
<div className={ stl.counter } data-hidden={ unReadNotificationsCount === 0 }>
|
||||
{ unReadNotificationsCount }
|
||||
</div>
|
||||
<Icon name="bell" size="18" />
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state: any) => ({
|
||||
notifications: state.getIn(['notifications', 'list']),
|
||||
}), { fetchList, setLastRead, setViewed, clearAll })(Notifications);
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import stl from './announcements.css';
|
||||
import stl from './announcements.module.css';
|
||||
import ListItem from './ListItem';
|
||||
import { connect } from 'react-redux';
|
||||
import { SlideModal, Icon, NoContent, Popup } from 'UI';
|
||||
import { fetchList, setLastRead } from 'Duck/announcements';
|
||||
import withToggle from 'Components/hocs/withToggle';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
@withToggle('visible', 'toggleVisisble')
|
||||
@withRouter
|
||||
|
|
@ -13,7 +14,7 @@ class Announcements extends React.Component {
|
|||
|
||||
navigateToUrl = url => {
|
||||
if (url) {
|
||||
if (url.startsWith(window.ENV.ORIGIN)) {
|
||||
if (url.startsWith(window.env.ORIGIN)) {
|
||||
const { history } = this.props;
|
||||
var path = new URL(url).pathname
|
||||
if (path.includes('/metrics')) {
|
||||
|
|
@ -44,20 +45,14 @@ class Announcements extends React.Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Popup
|
||||
trigger={
|
||||
<div className={ stl.button } onClick={ this.toggleModal } data-active={ visible }>
|
||||
<Popup content={ `Announcements` } >
|
||||
<div className={ stl.button } onClick={ this.toggleModal } data-active={ visible }>
|
||||
<div className={ stl.counter } data-hidden={ unReadNotificationsCount === 0 }>
|
||||
{ unReadNotificationsCount }
|
||||
</div>
|
||||
<Icon name="bullhorn" size="18" />
|
||||
</div>
|
||||
}
|
||||
content={ `Announcements` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
/>
|
||||
</Popup>
|
||||
|
||||
<SlideModal
|
||||
title="Announcements"
|
||||
|
|
@ -69,9 +64,13 @@ class Announcements extends React.Component {
|
|||
content={
|
||||
<div className="mx-4">
|
||||
<NoContent
|
||||
title=""
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="100" />
|
||||
</div>
|
||||
}
|
||||
subtext="There are no announcements to show."
|
||||
animatedIcon="no-results"
|
||||
// animatedIcon="no-results"
|
||||
show={ !loading && announcements.size === 0 }
|
||||
size="small"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Button, Label } from 'UI';
|
||||
import stl from './listItem.css';
|
||||
import stl from './listItem.module.css';
|
||||
|
||||
const ListItem = ({ announcement, onButtonClick }) => {
|
||||
return (
|
||||
|
|
@ -17,7 +17,7 @@ const ListItem = ({ announcement, onButtonClick }) => {
|
|||
<div className="mb-2 text-sm text-justify">{announcement.description}</div>
|
||||
{announcement.buttonUrl &&
|
||||
<Button
|
||||
primary outline size="small"
|
||||
variant="outline"
|
||||
onClick={() => onButtonClick(announcement.buttonUrl) }
|
||||
>
|
||||
<span className="capitalize">{announcement.buttonText}</span>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react'
|
||||
import stl from './ChatControls.css'
|
||||
import stl from './ChatControls.module.css'
|
||||
import cn from 'classnames'
|
||||
import { Button, Icon } from 'UI'
|
||||
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
|
||||
|
|
@ -29,14 +29,14 @@ function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled } : Props
|
|||
<div className={cn(stl.controls, "flex items-center w-full justify-start bottom-0 px-2")}>
|
||||
<div className="flex items-center">
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: audioEnabled})}>
|
||||
<Button plain size="small" onClick={toggleAudio} noPadding className="flex items-center" hover>
|
||||
<Button varient="text" onClick={toggleAudio} hover>
|
||||
<Icon name={audioEnabled ? 'mic' : 'mic-mute'} size="16" />
|
||||
<span className={cn("ml-1 color-gray-medium text-sm", { 'color-red' : audioEnabled })}>{audioEnabled ? 'Mute' : 'Unmute'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: videoEnabled})}>
|
||||
<Button plain size="small" onClick={toggleVideo} noPadding className="flex items-center" hover>
|
||||
<Button varient="text" onClick={toggleVideo} hover>
|
||||
<Icon name={ videoEnabled ? 'camera-video' : 'camera-video-off' } size="16" />
|
||||
<span className={cn("ml-1 color-gray-medium text-sm", { 'color-red' : videoEnabled })}>{videoEnabled ? 'Stop Video' : 'Start Video'}</span>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import VideoContainer from '../components/VideoContainer'
|
|||
import { Icon, Popup, Button } from 'UI'
|
||||
import cn from 'classnames'
|
||||
import Counter from 'App/components/shared/SessionItem/Counter'
|
||||
import stl from './chatWindow.css'
|
||||
import stl from './chatWindow.module.css'
|
||||
import ChatControls from '../ChatControls/ChatControls'
|
||||
import Draggable from 'react-draggable';
|
||||
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import RequestLocalStream from 'Player/MessageDistributor/managers/LocalStream';
|
|||
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
import stl from './AassistActions.css'
|
||||
import { confirm } from 'UI';
|
||||
import stl from './AassistActions.module.css'
|
||||
|
||||
function onClose(stream) {
|
||||
stream.getTracks().forEach(t=>t.stop());
|
||||
|
|
@ -114,25 +114,21 @@ function AssistActions({ toggleChatWindow, userId, calling, annotating, peerConn
|
|||
<div className={ stl.divider } />
|
||||
|
||||
<Popup
|
||||
trigger={
|
||||
<div
|
||||
className={
|
||||
cn(
|
||||
'cursor-pointer p-2 flex items-center',
|
||||
{[stl.disabled]: cannotCall}
|
||||
)
|
||||
}
|
||||
onClick={ onCall ? callObject?.end : confirmCall}
|
||||
role="button"
|
||||
>
|
||||
<IconButton size="small" primary={!onCall} red={onCall} label={onCall ? 'End' : 'Call'} icon="headset" />
|
||||
</div>
|
||||
}
|
||||
content={ cannotCall ? "You don’t have the permissions to perform this action." : `Call ${userId ? userId : 'User'}` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top right"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
className={
|
||||
cn(
|
||||
'cursor-pointer p-2 flex items-center',
|
||||
{[stl.disabled]: cannotCall}
|
||||
)
|
||||
}
|
||||
onClick={ onCall ? callObject?.end : confirmCall}
|
||||
role="button"
|
||||
>
|
||||
<IconButton size="small" primary={!onCall} red={onCall} label={onCall ? 'End' : 'Call'} icon="headset" />
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
|
||||
{ onCall && callObject && <ChatWindow endCall={callObject.end} userId={userId} incomeStream={incomeStream} localStream={localStream} /> }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { SlideModal, Avatar, TextEllipsis, Icon } from 'UI';
|
||||
import SessionList from '../SessionList';
|
||||
import stl from './assistTabs.css'
|
||||
import stl from './assistTabs.module.css'
|
||||
|
||||
interface Props {
|
||||
userId: any,
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Input, Dropdown, Button } from 'UI';
|
||||
import styles from './alertForm.css';
|
||||
|
||||
const periodOptions = [
|
||||
{
|
||||
text: '1 Week',
|
||||
value: 'week',
|
||||
},
|
||||
{
|
||||
text: '1 Month',
|
||||
value: 'month',
|
||||
},
|
||||
];
|
||||
|
||||
const AlertForm = ({
|
||||
alert, write, onSave, loading, onCancel = null,
|
||||
}) => (
|
||||
<div>
|
||||
<div className={ styles.formGroup }>
|
||||
<h3 className={ styles.label }>{'Title'}</h3>
|
||||
<Input
|
||||
name="name"
|
||||
value={ alert.name }
|
||||
onChange={ write }
|
||||
fluid
|
||||
placeholder="Name your alert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={ styles.formGroup }>
|
||||
<h3 className={ styles.label }>{'Threshold'}</h3>
|
||||
<Input
|
||||
type="number"
|
||||
name="countThreshold"
|
||||
value={ alert.countThreshold }
|
||||
fluid
|
||||
onChange={ write }
|
||||
placeholder="E.g. 20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={ styles.formGroup }>
|
||||
<h3 className={ styles.label }>{'For Next'}</h3>
|
||||
<Dropdown
|
||||
name="period"
|
||||
options={ periodOptions }
|
||||
value={ alert.period }
|
||||
fluid
|
||||
onChange={ write }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
loading={ loading }
|
||||
onClick={ onSave }
|
||||
content={ alert.id ? 'Update' : 'Set Alert' }
|
||||
outline
|
||||
disabled={ !alert.validate() }
|
||||
marginRight
|
||||
/>
|
||||
|
||||
{ onCancel &&
|
||||
<Button
|
||||
onClick={ onCancel }
|
||||
content="Cancel"
|
||||
outline
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AlertForm;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { Icon, SlideModal } from 'UI';
|
||||
import withToggle from 'HOCs/withToggle';
|
||||
import { save, edit } from 'Duck/alerts';
|
||||
|
||||
import styles from './alertManager.css';
|
||||
import AlertForm from './AlertForm';
|
||||
|
||||
@connect(state => ({
|
||||
alert: state.getIn([ 'alerts', 'instance' ]),
|
||||
loading: state.getIn([ 'alerts', 'saveRequest', 'loading' ]),
|
||||
filter: state.getIn([ 'filters', 'appliedFilter' ]),
|
||||
}), {
|
||||
save,
|
||||
edit,
|
||||
})
|
||||
@withToggle('isModalDisplayed', 'toggleModal')
|
||||
export default class AlertManager extends React.PureComponent {
|
||||
write = (e, { name, value }) => {
|
||||
this.props.edit({ [ name ]: value });
|
||||
}
|
||||
|
||||
save = () => {
|
||||
const { toggleModal, alert, filter } = this.props;
|
||||
this.props.save(alert.set('filter', filter))
|
||||
.then(toggleModal);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isModalDisplayed, alert, toggleModal, loading,
|
||||
} = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={ styles.button } onClick={ toggleModal }>
|
||||
<Icon name="search_notification" color="teal" size="16" />
|
||||
</div>
|
||||
<SlideModal
|
||||
title="Alert"
|
||||
isDisplayed={ isModalDisplayed }
|
||||
onClose={ toggleModal }
|
||||
size="small"
|
||||
content={
|
||||
<div className={ styles.wrapper }>
|
||||
<AlertForm
|
||||
alert={ alert }
|
||||
onSave={ this.save }
|
||||
write={ this.write }
|
||||
loading={ loading }
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
.formGroup {
|
||||
margin-bottom: 15px;
|
||||
|
||||
& .label {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
.wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.formGroup {
|
||||
margin-bottom: 15px;
|
||||
|
||||
& .label {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './AlertManager';
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react';
|
||||
import stl from './activeLabel.css';
|
||||
|
||||
const ActiveLabel = ({ item, onRemove }) => {
|
||||
return (
|
||||
<div className={ stl.wrapper } onClick={ () => onRemove(item) }>{ item.text }</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveLabel;
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { operatorOptions } from 'Types/filter';
|
||||
import { Icon } from 'UI';
|
||||
import { editAttribute, removeAttribute, applyFilter, fetchFilterOptions } from 'Duck/filters';
|
||||
import { debounce } from 'App/utils';
|
||||
import { KEYS } from 'Types/filter/customFilter';
|
||||
import stl from './attributeItem.css'
|
||||
import AttributeValueField from './AttributeValueField';
|
||||
import OperatorDropdown from './OperatorDropdown';
|
||||
import CustomFilters from '../CustomFilters';
|
||||
import FilterSelectionButton from '../FilterSelectionButton';
|
||||
|
||||
const DEFAULT = null;
|
||||
|
||||
@connect(state => ({
|
||||
loadingFilterOptions: state.getIn([ 'filters', 'fetchFilterOptions', 'loading' ]),
|
||||
filterOptions: state.getIn([ 'filters', 'filterOptions' ]),
|
||||
}), {
|
||||
editAttribute,
|
||||
removeAttribute,
|
||||
applyFilter,
|
||||
fetchFilterOptions
|
||||
})
|
||||
|
||||
class AttributeItem extends React.PureComponent {
|
||||
applyFilter = debounce(this.props.applyFilter, 1000)
|
||||
fetchFilterOptionsDebounce = debounce(this.props.fetchFilterOptions, 500)
|
||||
|
||||
onFilterChange = (name, value, valueIndex) => {
|
||||
const { index } = this.props;
|
||||
this.props.editAttribute(index, name, value, valueIndex);
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
removeFilter = () => {
|
||||
const { index } = this.props;
|
||||
this.props.removeAttribute(index)
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
handleSearchChange = (e, { searchQuery }) => {
|
||||
const { filter } = this.props;
|
||||
this.fetchFilterOptionsDebounce(filter, searchQuery);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, options, index, loadingFilterOptions, filterOptions } = this.props;
|
||||
const _operatorOptions = operatorOptions(filter);
|
||||
|
||||
let filterLabel = filter.label;
|
||||
if (filter.type === KEYS.METADATA)
|
||||
filterLabel = filter.key;
|
||||
|
||||
return (
|
||||
<div className={ stl.wrapper }>
|
||||
<CustomFilters
|
||||
index={ index }
|
||||
filter={ filter }
|
||||
buttonComponent={ <FilterSelectionButton label={ filterLabel } />}
|
||||
showFilters={ true }
|
||||
filterType="filter"
|
||||
/>
|
||||
{ filter.type !== KEYS.DURATION &&
|
||||
<OperatorDropdown
|
||||
options={ _operatorOptions }
|
||||
onChange={ this.onFilterChange }
|
||||
value={ filter.operator || DEFAULT }
|
||||
/>
|
||||
}
|
||||
{
|
||||
// !filter.hasNoValue &&
|
||||
<AttributeValueField
|
||||
filter={ filter }
|
||||
options={ options }
|
||||
onChange={ this.onFilterChange }
|
||||
handleSearchChange={this.handleSearchChange}
|
||||
loading={loadingFilterOptions}
|
||||
index={index}
|
||||
/>
|
||||
}
|
||||
|
||||
<div className={ stl.actions }>
|
||||
<button className={ stl.button } onClick={ this.removeFilter }>
|
||||
<Icon name="close" size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AttributeItem;
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import stl from './attributeItem.css'
|
||||
import { Dropdown } from 'semantic-ui-react';
|
||||
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, updateValue } from 'Duck/filters';
|
||||
import DurationFilter from '../DurationFilter/DurationFilter';
|
||||
import AutoComplete from '../AutoComplete';
|
||||
|
||||
const DEFAULT = null;
|
||||
|
||||
const getHeader = (type) => {
|
||||
if (type === 'LOCATION') return 'Path';
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
@connect(null, {
|
||||
setActiveKey,
|
||||
addCustomFilter,
|
||||
removeCustomFilter,
|
||||
applyFilter,
|
||||
updateValue,
|
||||
})
|
||||
class AttributeValueField extends React.PureComponent {
|
||||
state = {
|
||||
minDuration: this.props.filter.minDuration,
|
||||
maxDuration: this.props.filter.maxDuration,
|
||||
}
|
||||
|
||||
onValueChange = (e, { name: key, value }) => {
|
||||
this.props.addCustomFilter(key, value);
|
||||
};
|
||||
|
||||
onDurationChange = (durationValues) => {
|
||||
this.setState(durationValues);
|
||||
}
|
||||
|
||||
isAutoComplete = (type) => {
|
||||
switch (type) {
|
||||
case TYPES.METADATA:
|
||||
case TYPES.CLICK:
|
||||
case TYPES.CONSOLE:
|
||||
case TYPES.GRAPHQL:
|
||||
case TYPES.FETCH:
|
||||
case TYPES.STATEACTION:
|
||||
case TYPES.USERID:
|
||||
case TYPES.USERANONYMOUSID:
|
||||
case TYPES.REVID:
|
||||
case TYPES.GRAPHQL:
|
||||
case TYPES.CUSTOM:
|
||||
case TYPES.LOCATION:
|
||||
case TYPES.VIEW:
|
||||
case TYPES.INPUT:
|
||||
case 'metadata':
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
handleClose = (e) => {
|
||||
const { filter, onChange } = this.props;
|
||||
if (filter.key === KEYS.DURATION) {
|
||||
const { maxDuration, minDuration, key } = filter;
|
||||
if (maxDuration || minDuration) return;
|
||||
if (maxDuration !== this.state.maxDuration ||
|
||||
minDuration !== this.state.minDuration) {
|
||||
onChange(e, { name: 'value', value: [this.state.minDuration, this.state.maxDuration] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderField() {
|
||||
const { filter, onChange } = this.props;
|
||||
|
||||
if (filter.key === KEYS.DURATION) {
|
||||
const { maxDuration, minDuration } = this.state;
|
||||
return (
|
||||
<DurationFilter
|
||||
onChange={ this.onDurationChange }
|
||||
onEnterPress={ this.handleClose }
|
||||
onBlur={this.handleClose}
|
||||
minDuration={ minDuration }
|
||||
maxDuration={ maxDuration }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { options = [], handleSearchChange, loading } = this.props;
|
||||
return (
|
||||
<Dropdown
|
||||
className={ cn(stl.filterDropdown) }
|
||||
placeholder="Select"
|
||||
name="value"
|
||||
search
|
||||
selection
|
||||
value={ filter.value || DEFAULT }
|
||||
options={ options }
|
||||
multiple={options.length > 0 || options.size > 0}
|
||||
onChange={ onChange }
|
||||
onSearchChange={handleSearchChange}
|
||||
icon={ null }
|
||||
noResultsMessage={loading ? <div>
|
||||
<CircularLoader loading={ loading } style={ { marginRight: '8px' } } />
|
||||
</div>: 'No results found.'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
optionMapping = (values) => {
|
||||
const { filter } = this.props;
|
||||
if ([KEYS.USER_DEVICE, KEYS.USER_OS, KEYS.USER_BROWSER, KEYS.REFERRER, KEYS.PLATFORM].indexOf(filter.type) !== -1) {
|
||||
return values.map(item => ({ type: TYPES.METADATA, value: item })).map(CustomFilter);
|
||||
} else {
|
||||
return values.map(Event);
|
||||
}
|
||||
}
|
||||
|
||||
getParams = filter => {
|
||||
const params = {};
|
||||
|
||||
if (filter.type === TYPES.METADATA) {
|
||||
params.key = filter.key
|
||||
}
|
||||
|
||||
params.type = filter.type
|
||||
if (filter.type === TYPES.ERROR && filter.source) {
|
||||
params.source = filter.source
|
||||
}
|
||||
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 } = this.props;
|
||||
const { filter } = this.props;
|
||||
const _showAutoComplete = this.isAutoComplete(filter.type);
|
||||
const _params = _showAutoComplete ? this.getParams(filter) : {};
|
||||
let _optionsEndpoint= '/events/search';
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ _showAutoComplete ? filter.value.map((v, i) => (
|
||||
<AutoComplete
|
||||
name={ 'value' }
|
||||
endpoint={ _optionsEndpoint }
|
||||
value={ v }
|
||||
index={ i }
|
||||
params={ _params }
|
||||
optionMapping={this.optionMapping}
|
||||
onSelect={ (e, { name, value }) => onChange(name, value, i) }
|
||||
headerText={ <h5 className={ stl.header }>{ getHeader(filter.type) }</h5> }
|
||||
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' &&
|
||||
<LinkStyledInput
|
||||
displayLabel="Specify value"
|
||||
placeholder="Specify value"
|
||||
name="custom"
|
||||
onChange={ onChange }
|
||||
value={filter.custom}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AttributeValueField;
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { countries } from 'App/constants';
|
||||
import { KEYS } from 'Types/filter/customFilter';
|
||||
import { addAttribute } from 'Duck/filters';
|
||||
import AttributeItem from './AttributeItem';
|
||||
import ListHeader from '../ListHeader';
|
||||
import logger from 'App/logger';
|
||||
|
||||
const DEFAULT = null;
|
||||
const DEFAULT_OPTION = { text: 'Any', value: DEFAULT };
|
||||
const toOptions = (values, mapper) => (values ? values
|
||||
.map(({value}) => ({
|
||||
text: mapper ? mapper[ value ] : value,
|
||||
value,
|
||||
}))
|
||||
.toJS() : [ DEFAULT_OPTION ]);
|
||||
|
||||
const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i }));
|
||||
|
||||
@connect(state => ({
|
||||
filters: state.getIn([ 'filters', 'appliedFilter', 'filters' ]),
|
||||
filterValues: state.get('filterValues'),
|
||||
filterOptions: state.getIn([ 'filters', 'filterOptions' ]),
|
||||
}), {
|
||||
addAttribute,
|
||||
})
|
||||
class Attributes extends React.PureComponent {
|
||||
getOptions = filter => {
|
||||
const { filterValues, filterOptions } = this.props;
|
||||
|
||||
if (filter.key === KEYS.USER_COUNTRY) {
|
||||
logger.log('Filters: country')
|
||||
return countryOptions;
|
||||
}
|
||||
|
||||
if (filter.key === KEYS.METADATA) {
|
||||
logger.log('Filters: metadata ' + filter.key)
|
||||
const options = filterValues.get(filter.key);
|
||||
return options && options.size ? toOptions(options) : [];
|
||||
}
|
||||
|
||||
logger.log('Filters: general filters ' + filter.key)
|
||||
const options = filterOptions.get(filter.key)
|
||||
return options && options.size ? toOptions(options.filter(i => !!i)) : []
|
||||
}
|
||||
render() {
|
||||
const { filters } = this.props;
|
||||
return (
|
||||
<>
|
||||
{ filters.size > 0 &&
|
||||
<div>
|
||||
<div className="py-1"><ListHeader title="Filters" /></div>
|
||||
{
|
||||
filters.map((filter, index) => (
|
||||
<AttributeItem
|
||||
key={index}
|
||||
index={ index }
|
||||
filter={ filter }
|
||||
options={ this.getOptions(filter) }
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Attributes;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Dropdown, Icon } from 'UI';
|
||||
import stl from './attributeItem.css'
|
||||
|
||||
const OperatorDropdown = ({ options, value, onChange }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
className={ cn(stl.operatorDropdown) }
|
||||
options={ options }
|
||||
name="operator"
|
||||
value={ value }
|
||||
onChange={ onChange }
|
||||
icon={ <Icon className="ml-5" name="chevron-down" size="12" /> }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperatorDropdown;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
.wrapper {
|
||||
padding: 3px 8px;
|
||||
background-color: $gray-lightest;
|
||||
border-radius: 10px;
|
||||
margin-right: 5px;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05) inset;
|
||||
font-size: 13px;
|
||||
color: $gray-medium;
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
@import 'icons.css';
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 15px;
|
||||
background-color: white;
|
||||
border-bottom: solid thin $gray-lightest;
|
||||
&:last-child {
|
||||
border-bottom: solid thin transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
& .actions {
|
||||
opacity: 1;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
& > div:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
& .label {
|
||||
font-weight: 600;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
& .filterDropdown {
|
||||
/* height: 28px !important; */
|
||||
padding: 0 5px !important;
|
||||
min-height: 28px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
font-weight: 400;
|
||||
min-width: 280px !important;
|
||||
max-width: 75% !important;
|
||||
flex-wrap: wrap;
|
||||
padding: 1.9px !important;
|
||||
background-color: rgba(255, 255, 255, 0.8) !important;
|
||||
padding-left: 5px !important;
|
||||
|
||||
& a {
|
||||
background-color: $gray-lightest !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
white-space: nowrap !important;
|
||||
margin: 0 !important;
|
||||
margin-right: 5px !important;
|
||||
margin-bottom: 2px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding: 3px 5px !important;
|
||||
|
||||
& i::before {
|
||||
display: none;
|
||||
}
|
||||
& i::after {
|
||||
content: '' !important;
|
||||
@mixin icon close, $gray-dark, 12px;
|
||||
}
|
||||
}
|
||||
|
||||
& input {
|
||||
padding: 6px !important;
|
||||
margin: 0 !important;
|
||||
color: $gray-medium !important;
|
||||
}
|
||||
|
||||
& .delete.icon {
|
||||
padding: 0 !important;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operatorDropdown {
|
||||
font-weight: 400;
|
||||
height: 28px;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
transition: all 0.4s;
|
||||
}
|
||||
|
||||
.inputValue {
|
||||
height: 28px !important;
|
||||
width: 180px;
|
||||
color: $gray-medium !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: #596764;
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
letter-spacing: 0.1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Attributes';
|
||||
|
|
@ -5,7 +5,7 @@ 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 stl from './autoComplete.module.css';
|
||||
import FilterItem from '../CustomFilters/FilterItem';
|
||||
|
||||
const TYPE_TO_SEARCH_MSG = "Start typing to search...";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import stl from './dropdownItem.css';
|
||||
import stl from './dropdownItem.module.css';
|
||||
|
||||
const DropdownItem = ({ value, onSelect }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
|
|
@ -8,11 +9,10 @@ import { applyFilter, clearEvents, addAttribute } from 'Duck/filters';
|
|||
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
|
||||
import { KEYS } from 'Types/filter/customFilter';
|
||||
import SessionList from './SessionList';
|
||||
import stl from './bugFinder.css';
|
||||
import stl from './bugFinder.module.css';
|
||||
import withLocationHandlers from "HOCs/withLocationHandlers";
|
||||
import { fetch as fetchFilterVariables } from 'Duck/sources';
|
||||
import { fetchSources } from 'Duck/customField';
|
||||
import { RehydrateSlidePanel } from './WatchDogs/components';
|
||||
import { setFunnelPage } from 'Duck/sessions';
|
||||
import { setActiveTab } from 'Duck/search';
|
||||
import SessionsMenu from './SessionsMenu/SessionsMenu';
|
||||
|
|
@ -137,10 +137,6 @@ export default class BugFinder extends React.PureComponent {
|
|||
<SessionList onMenuItemClick={this.setActiveTab} />
|
||||
</div>
|
||||
</div>
|
||||
<RehydrateSlidePanel
|
||||
isModalDisplayed={ showRehydratePanel }
|
||||
onClose={ () => this.setState({ showRehydratePanel: false })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { addEvent, applyFilter, setActiveKey, addAttribute } from 'Duck/filters';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import FilterModal from './FilterModal';
|
||||
|
||||
export default React.memo(function CustomFilters({
|
||||
index,
|
||||
buttonComponent,
|
||||
filterType,
|
||||
}) {
|
||||
const [ displayed, setDisplayed ] = useState(false);
|
||||
const close = useCallback(() => setDisplayed(false), []);
|
||||
const toggle = useCallback(() => setDisplayed(d => !d), []);
|
||||
|
||||
return (
|
||||
<OutsideClickDetectingDiv className="relative" onClickOutside={ close }>
|
||||
<div role="button" onClick={ toggle }>{ buttonComponent || 'Add Step' }</div>
|
||||
<FilterModal
|
||||
index={ index }
|
||||
close={ close }
|
||||
displayed={ displayed }
|
||||
filterType={ filterType }
|
||||
/>
|
||||
|
||||
</OutsideClickDetectingDiv>
|
||||
);
|
||||
})
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import stl from './filterItem.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
const FilterItem = ({ className = '', icon, label, onClick }) => {
|
||||
return (
|
||||
<div className={ cn(stl.filterItem, className) } id="filter-item" onClick={ onClick }>
|
||||
{ icon && <Icon name={ icon } size="16" marginRight="8" /> }
|
||||
<span className={ stl.label }>{ label }</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterItem;
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { List } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
import { getRE } from 'App/utils';
|
||||
import { defaultFilters, preloadedFilters } from 'Types/filter';
|
||||
import { TYPES } from 'Types/filter/event';
|
||||
import CustomFilter, { KEYS } from 'Types/filter/customFilter';
|
||||
import { applyFilter, setActiveKey, addEvent, removeEvent, setFilterOption, changeEvent, addAttribute, removeAttribute } from 'Duck/filters';
|
||||
import { NoContent, CircularLoader } from 'UI';
|
||||
import { debounce } from 'App/utils';
|
||||
import FilterItem from './FilterItem';
|
||||
import logger from 'App/logger';
|
||||
|
||||
import stl from './filterModal.css';
|
||||
|
||||
const customFilterAutoCompleteKeys = ['METADATA', KEYS.CLICK, KEYS.USER_BROWSER, KEYS.USER_OS, KEYS.USER_DEVICE, KEYS.REFERRER]
|
||||
|
||||
@connect(state => ({
|
||||
filter: state.getIn([ 'filters', 'appliedFilter' ]),
|
||||
customFilters: state.getIn([ 'filters', 'customFilters' ]),
|
||||
variables: state.getIn([ 'customFields', 'list' ]),
|
||||
sources: state.getIn([ 'customFields', 'sources' ]),
|
||||
activeTab: state.getIn([ 'sessions', 'activeTab', 'type' ]),
|
||||
}), {
|
||||
applyFilter,
|
||||
setActiveKey,
|
||||
addEvent,
|
||||
removeEvent,
|
||||
addAttribute,
|
||||
removeAttribute,
|
||||
setFilterOption
|
||||
})
|
||||
export default class FilterModal extends React.PureComponent {
|
||||
state = { query: '' }
|
||||
applyFilter = debounce(this.props.applyFilter, 300);
|
||||
|
||||
onFilterClick = (filter, apply) => {
|
||||
const key = filter.key || filter.type;
|
||||
if (customFilterAutoCompleteKeys.includes(key)) {
|
||||
this.props.setFilterOption(key, filter.value ? [{value: filter.value[0], type: key}] : [])
|
||||
}
|
||||
this.addFilter(filter);
|
||||
if (apply || filter.hasNoValue) {
|
||||
this.applyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
renderFilterItem(type, filter) {
|
||||
return (
|
||||
<FilterItem
|
||||
className="capitalize"
|
||||
label={ filter.label || filter.key }
|
||||
icon={ filter.icon }
|
||||
onClick={ () => this.onFilterClick(filter) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
addFilter = (filter) => {
|
||||
const { index, filterType, filter: { filters } } = this.props;
|
||||
this.props.close();
|
||||
|
||||
if (filter.isFilter || filter.type === 'METADATA') {
|
||||
logger.log('Adding Filter', filter)
|
||||
const _index = filterType === 'filter' ? index : undefined; // should add new one if coming from events
|
||||
const _in = filters.findIndex(e => e.type === 'USERID');
|
||||
this.props.addAttribute(filter, _in >= 0 ? _in : _index);
|
||||
} else {
|
||||
logger.log('Adding Event', filter)
|
||||
const _index = filterType === 'event' ? index : undefined; // should add new one if coming from filters
|
||||
this.props.addEvent(filter, false, _index);
|
||||
}
|
||||
|
||||
if (filterType === 'event' && filter.isFilter) { // selected a filter from events
|
||||
this.props.removeEvent(index);
|
||||
}
|
||||
|
||||
if (filterType === 'filter' && !filter.isFilter) { // selected an event from filters
|
||||
this.props.removeAttribute(index);
|
||||
}
|
||||
};
|
||||
|
||||
renderList(type, list) {
|
||||
const { activeTab } = this.props;
|
||||
const blocks = [];
|
||||
for (let j = 0; j < list.length; j++) {
|
||||
blocks.push(
|
||||
<div key={`${ j }-block`} className={cn("mr-5", { [stl.disabled]: activeTab === 'live' && list[j].key !== 'USERID' })} >
|
||||
{ list[ j ] && this.renderFilterItem(type, list[ j ]) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
test = (value = '') => getRE(this.props.searchQuery, 'i').test(value);
|
||||
|
||||
renderEventDropdownItem = filter => (
|
||||
<FilterItem
|
||||
key={ filter.actualValue || filter.value }
|
||||
label={ filter.actualValue || filter.value }
|
||||
icon={ filter.icon }
|
||||
onClick={ () => this.onFilterClick(filter, true) }
|
||||
/>
|
||||
)
|
||||
|
||||
renderEventDropdownPartFromList = (list, headerText) => (list.size > 0 &&
|
||||
<div className={ cn(stl.filterGroupApi, 'mb-2') }>
|
||||
<h5 className={ stl.header }>{ headerText }</h5>
|
||||
{ list.map(this.renderEventDropdownItem) }
|
||||
</div>
|
||||
)
|
||||
|
||||
renderEventDropdownPart = (type, headerText) => {
|
||||
const searched = this.props.searchedEvents
|
||||
.filter(e => e.type === type)
|
||||
.filter(({ value, target }) => !this.props.loading || this.test(value) || this.test(target && target.label));
|
||||
|
||||
return this.renderEventDropdownPartFromList(searched, headerText)
|
||||
};
|
||||
|
||||
renderStaticFiltersDropdownPart = (type, headerText, appliedFilterKeys) => {
|
||||
if (appliedFilterKeys && appliedFilterKeys.includes(type)) return;
|
||||
const staticFilters = List(preloadedFilters)
|
||||
.filter(e => e.type === type)
|
||||
.filter(({ value, actualValue }) => this.test(actualValue || value))
|
||||
.map(CustomFilter);
|
||||
|
||||
return this.renderEventDropdownPartFromList(staticFilters, headerText)
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
displayed,
|
||||
customFilters,
|
||||
filter,
|
||||
loading = false,
|
||||
searchedEvents,
|
||||
searchQuery = '',
|
||||
activeTab,
|
||||
} = this.props;
|
||||
const { query } = this.state;
|
||||
const reg = getRE(query, 'i');
|
||||
|
||||
const _appliedFilterKeys = filter.filters.map(({type}) => type).toJS();
|
||||
const filteredList = defaultFilters.map(cat => {
|
||||
let _keys = [];
|
||||
if (query.length === 0 && cat.type === 'custom') { // default show limited custom fields
|
||||
_keys = cat.keys.slice(0, 9).filter(({key}) => reg.test(key))
|
||||
} else {
|
||||
_keys = cat.keys.filter(({key}) => reg.test(key));
|
||||
}
|
||||
return {
|
||||
...cat,
|
||||
keys: _keys
|
||||
.filter(({key, filterKey}) => !_appliedFilterKeys.includes(filterKey) && !customFilters.has(filterKey || key) && !filter.get(filterKey || key))
|
||||
}
|
||||
}).filter(cat => cat.keys.length > 0);
|
||||
|
||||
const staticFilters = preloadedFilters
|
||||
.filter(({ value, actualValue }) => !this.props.loading && this.test(actualValue || value))
|
||||
|
||||
return (!displayed ? null :
|
||||
<div className={ stl.modal }>
|
||||
{ loading &&
|
||||
<div style={ {marginBottom: '20px'}}><CircularLoader loading={ loading } /></div>
|
||||
}
|
||||
|
||||
<NoContent
|
||||
title="No results found."
|
||||
size="small"
|
||||
show={ searchQuery !== '' && !loading && staticFilters.length === 0 && searchedEvents.size === 0 }
|
||||
>
|
||||
<div className={ stl.filterListDynamic }>
|
||||
{ searchQuery &&
|
||||
<React.Fragment>
|
||||
{this.renderEventDropdownPart(TYPES.USERID, 'User Id')}
|
||||
{activeTab !== 'live' && (
|
||||
<>
|
||||
{this.renderEventDropdownPart(TYPES.METADATA, 'Metadata')}
|
||||
{this.renderEventDropdownPart(TYPES.CONSOLE, 'Errors')}
|
||||
{this.renderEventDropdownPart(TYPES.CUSTOM, 'Custom Events')}
|
||||
{this.renderEventDropdownPart(KEYS.USER_COUNTRY, 'Country', _appliedFilterKeys)}
|
||||
{this.renderEventDropdownPart(KEYS.USER_BROWSER, 'Browser', _appliedFilterKeys)}
|
||||
{this.renderEventDropdownPart(KEYS.USER_DEVICE, 'Device', _appliedFilterKeys)}
|
||||
{this.renderEventDropdownPart(TYPES.LOCATION, 'Page')}
|
||||
{this.renderEventDropdownPart(TYPES.CLICK, 'Click')}
|
||||
{this.renderEventDropdownPart(TYPES.FETCH, 'Fetch')}
|
||||
{this.renderEventDropdownPart(TYPES.INPUT, 'Input')}
|
||||
|
||||
{this.renderEventDropdownPart(KEYS.USER_OS, 'Operating System', _appliedFilterKeys)}
|
||||
{this.renderEventDropdownPart(KEYS.REFERRER, 'Referrer', _appliedFilterKeys)}
|
||||
{this.renderEventDropdownPart(TYPES.GRAPHQL, 'GraphQL')}
|
||||
{this.renderEventDropdownPart(TYPES.STATEACTION, 'Store Action')}
|
||||
{this.renderEventDropdownPart(TYPES.REVID, 'Rev ID')}
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
{ searchQuery === '' &&
|
||||
<div className={ stl.filterListStatic }>
|
||||
{
|
||||
filteredList.map(category => (
|
||||
<div className={ cn(stl.filterGroup, 'mr-6 mb-6') } key={category.category}>
|
||||
<h5 className={ stl.header }>{ category.category }</h5>
|
||||
<div className={ stl.list }>
|
||||
{ this.renderList(category.type, category.keys) }
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</NoContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
.modal {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
width: -webkit-fill-available;
|
||||
min-width: 705px;
|
||||
max-width: calc(100vw - 500px);
|
||||
border-radius: 3px;
|
||||
border: solid thin $gray-light;
|
||||
box-shadow: 0 2px 10px 0 $gray-light;
|
||||
z-index: 99;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: $gray-light;
|
||||
font-size: 12px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
h5.title {
|
||||
margin: 10px 0 3px;
|
||||
}
|
||||
|
||||
.filterListDynamic {
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $gray-medium;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
& .header {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: #596764;
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
& .list {
|
||||
margin-left: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterListStatic {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
max-height: 33rem;
|
||||
min-height: 20px;
|
||||
color: $gray-medium;
|
||||
|
||||
& .header {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: #596764;
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
& .list {
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
& .filterGroup {
|
||||
width: 205px;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CustomFilters';
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { applyFilter } from 'Duck/search';
|
||||
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
import { Input, Label } from 'semantic-ui-react';
|
||||
import styles from './durationFilter.css';
|
||||
|
||||
const fromMs = value => value ? `${ value / 1000 / 60 }` : ''
|
||||
const toMs = value => value !== '' ? value * 1000 * 60 : null
|
||||
|
||||
export default class DurationFilter extends React.PureComponent {
|
||||
state = { focused: false }
|
||||
onChange = (e, { name, value }) => {
|
||||
const { onChange } = this.props;
|
||||
if (typeof onChange === 'function') {
|
||||
onChange({
|
||||
[ name ]: toMs(value),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onKeyPress = e => {
|
||||
const { onEnterPress } = this.props;
|
||||
if (e.key === 'Enter' && typeof onEnterPress === 'function') {
|
||||
onEnterPress(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
minDuration,
|
||||
maxDuration,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={ styles.wrapper }>
|
||||
<Input
|
||||
labelPosition="left"
|
||||
type="number"
|
||||
placeholder="0 min"
|
||||
name="minDuration"
|
||||
value={ fromMs(minDuration) }
|
||||
onChange={ this.onChange }
|
||||
className="customInput"
|
||||
onKeyPress={ this.onKeyPress }
|
||||
onFocus={() => this.setState({ focused: true })}
|
||||
onBlur={this.props.onBlur}
|
||||
>
|
||||
<Label basic className={ styles.label }>{ 'Min' }</Label>
|
||||
<input min="1" />
|
||||
</Input>
|
||||
<Input
|
||||
labelPosition="left"
|
||||
type="number"
|
||||
placeholder="∞ min"
|
||||
name="maxDuration"
|
||||
value={ fromMs(maxDuration) }
|
||||
onChange={ this.onChange }
|
||||
className="customInput"
|
||||
onKeyPress={ this.onKeyPress }
|
||||
onFocus={() => this.setState({ focused: true })}
|
||||
onBlur={this.props.onBlur}
|
||||
>
|
||||
<Label basic className={ styles.label }>{ 'Max' }</Label>
|
||||
<input min="1" />
|
||||
</Input>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& input {
|
||||
max-width: 85px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
color: $gray-medium !important;
|
||||
}
|
||||
|
||||
& > div {
|
||||
&:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
color: $gray-medium !important;
|
||||
}
|
||||
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { TYPES } from 'Types/filter/event';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import cls from './eventDropdownItem.css';
|
||||
|
||||
|
||||
const getText = (event) => {
|
||||
if (event.type === TYPES.METADATA) {
|
||||
return `${ event.key }: ${ event.value }`;
|
||||
}
|
||||
if (event.target) {
|
||||
return event.target.label || event.value;
|
||||
}
|
||||
return event.value; // both should be?
|
||||
};
|
||||
|
||||
export default function EventDropdownItem({ event }) {
|
||||
return (
|
||||
<div className={ cn("flex items-center", cls.eventDropdownItem) }>
|
||||
<Icon name={ event.icon } size="14" marginRight="10" />
|
||||
<div
|
||||
className={ cn(cls.values,{
|
||||
[ cls.inputType ]: event.type === TYPES.INPUT,
|
||||
[ cls.clickType ]: event.type === TYPES.CLICK,
|
||||
[ cls.consoleType ]: event.type === TYPES.CONSOLE,
|
||||
})}
|
||||
>
|
||||
{ getText(event) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
// import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
|
||||
import Event, { TYPES } from 'Types/filter/event';
|
||||
import { operatorOptions } from 'Types/filter';
|
||||
import { editEvent, removeEvent, clearEvents, applyFilter } from 'Duck/filters';
|
||||
import { Icon } from 'UI';
|
||||
import stl from './eventEditor.css';
|
||||
import { debounce } from 'App/utils';
|
||||
import AttributeValueField from '../Attributes/AttributeValueField';
|
||||
import OperatorDropdown from '../Attributes/OperatorDropdown';
|
||||
import CustomFilters from '../CustomFilters';
|
||||
import FilterSelectionButton from '../FilterSelectionButton';
|
||||
|
||||
const getPlaceholder = ({ type }) => {
|
||||
if (type === TYPES.INPUT) return "E.g. First Name";
|
||||
if (type === TYPES.LOCATION) return "Specify URL / Path";
|
||||
if (type === TYPES.VIEW) return "Specify View Name";
|
||||
if (type === TYPES.CONSOLE) return "Specify Error Message";
|
||||
if (type === TYPES.CUSTOM) return "Specify Custom Event Name";
|
||||
return '';
|
||||
};
|
||||
|
||||
const getLabel = ({ type }) => {
|
||||
if (type === TYPES.INPUT) return "Specify Value";
|
||||
return getPlaceholder({ type });
|
||||
};
|
||||
|
||||
// @DNDTarget('event')
|
||||
// @DNDSource('event')
|
||||
@connect(state => ({
|
||||
isLastEvent: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 1,
|
||||
}), { editEvent, removeEvent, clearEvents, applyFilter })
|
||||
export default class EventEditor extends React.PureComponent {
|
||||
applyFilter = debounce(this.props.applyFilter, 1500)
|
||||
|
||||
onChange = (e, { name, value, searchType }) => {
|
||||
const { index } = this.props;
|
||||
const updFields = { [name]: value };
|
||||
if (searchType != null) {
|
||||
updFields.searchType = searchType;
|
||||
}
|
||||
this.props.editEvent(index, updFields);
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
onTargetChange = (e, {target}) => {
|
||||
const { index, event } = this.props;
|
||||
this.props.editEvent(index, {target});
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
onCheckboxChange = ({ target: { name, checked }}) => {
|
||||
this.props.editEvent(this.props.index, name, checked);
|
||||
}
|
||||
|
||||
remove = () => {
|
||||
this.props.removeEvent(this.props.index);
|
||||
this.applyFilter()
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
event,
|
||||
index,
|
||||
isDragging,
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
} = this.props;
|
||||
|
||||
const _operatorOptions = operatorOptions(event);
|
||||
|
||||
const dndBtn = connectDragSource(
|
||||
<button className={ stl.button }><Icon name="drag" size="16" /></button>
|
||||
);
|
||||
|
||||
return connectDropTarget(
|
||||
<div className={ stl.wrapper } style={ isDragging ? { opacity: 0.5 } : null } >
|
||||
<div className={ stl.leftSection }>
|
||||
<div className={ stl.index }>{ index + 1 }</div>
|
||||
|
||||
<CustomFilters
|
||||
index={ index }
|
||||
filter={ event }
|
||||
buttonComponent={ <FilterSelectionButton label={ (event.source && event.source !== 'js_exception') ? event.source : event.label } />}
|
||||
filterType="event"
|
||||
/>
|
||||
|
||||
<OperatorDropdown
|
||||
options={ _operatorOptions }
|
||||
onChange={ this.onChange }
|
||||
value={ event.operator || DEFAULT }
|
||||
/>
|
||||
|
||||
<AttributeValueField
|
||||
filter={ event }
|
||||
onChange={ this.onChange }
|
||||
onTargetChange={ this.onTargetChange }
|
||||
/>
|
||||
</div>
|
||||
<div className={ stl.actions }>
|
||||
{ dndBtn }
|
||||
<button className={ stl.button } onClick={ this.remove }>
|
||||
<Icon name="close" size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { Input } from 'semantic-ui-react';
|
||||
// import { DNDContext } from 'Components/hocs/dnd';
|
||||
import {
|
||||
addEvent, applyFilter, moveEvent, clearEvents, edit,
|
||||
addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
|
||||
} from 'Duck/filters';
|
||||
import { fetchList as fetchEventList } from 'Duck/events';
|
||||
import { debounce } from 'App/utils';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import EventEditor from './EventEditor';
|
||||
import ListHeader from '../ListHeader';
|
||||
import FilterModal from '../CustomFilters/FilterModal';
|
||||
import { IconButton, SegmentSelection } from 'UI';
|
||||
import stl from './eventFilter.css';
|
||||
import Attributes from '../Attributes/Attributes';
|
||||
import RandomPlaceholder from './RandomPlaceholder';
|
||||
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' ]),
|
||||
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' ]),
|
||||
}), {
|
||||
applyFilter,
|
||||
addEvent,
|
||||
moveEvent,
|
||||
fetchEventList,
|
||||
clearEvents,
|
||||
addCustomFilter,
|
||||
addAttribute,
|
||||
setSearchQuery,
|
||||
setActiveFlow,
|
||||
setFilterOption,
|
||||
setBlink,
|
||||
edit,
|
||||
})
|
||||
// @DNDContext
|
||||
export default class EventFilter extends React.PureComponent {
|
||||
state = { search: '', showFilterModal: false, showPlacehoder: true }
|
||||
fetchEventList = debounce(this.props.fetchEventList, 500)
|
||||
inputRef = React.createRef()
|
||||
|
||||
componentDidUpdate(){
|
||||
const { blink, setBlink } = this.props;
|
||||
if (blink) {
|
||||
setTimeout(function() {
|
||||
setBlink(false)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
const { searchQuery } = this.props;
|
||||
this.setState({ showPlacehoder: searchQuery === '' });
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({ showPlacehoder: false, showFilterModal: true });
|
||||
}
|
||||
|
||||
onChangeStrict = () => {
|
||||
this.props.applyFilter({ strict: !this.props.strict });
|
||||
}
|
||||
|
||||
onSearchChange = (e, { value }) => {
|
||||
this.props.setSearchQuery(value)
|
||||
if (value !== '') this.fetchEventList({ q: value });
|
||||
}
|
||||
|
||||
onPlaceholderClick = () => {
|
||||
this.inputRef.current && this.inputRef.current.focus();
|
||||
}
|
||||
|
||||
closeModal = () => {
|
||||
this.setState({ showPlacehoder: true, showFilterModal: false })
|
||||
}
|
||||
|
||||
onPlaceholderItemClick = (e, filter) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (Array.isArray(filter)) {
|
||||
for (var i = 0; i < filter.length; i++) {
|
||||
this.onPlaceholderItemClick(e, filter[i]);
|
||||
}
|
||||
} else if (filter.isFilter) {
|
||||
this.props.setFilterOption(filter.key, [{ value: filter.value[0], type: filter.key }])
|
||||
this.props.addAttribute(filter);
|
||||
}
|
||||
else
|
||||
this.props.addEvent(filter);
|
||||
|
||||
if (filter.value || filter.hasNoValue) {
|
||||
this.props.applyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
clearEvents = () => {
|
||||
this.props.clearEvents();
|
||||
this.props.setActiveFlow(null)
|
||||
}
|
||||
|
||||
changeConditionTab = (e, { name, value }) => {
|
||||
this.props.edit({ [ 'condition' ]: value })
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
events,
|
||||
loading,
|
||||
searchedEvents,
|
||||
appliedFilterKeys,
|
||||
appliedFilter,
|
||||
searchQuery,
|
||||
blink
|
||||
} = this.props;
|
||||
const { showFilterModal, showPlacehoder } = this.state;
|
||||
const hasFilters = appliedFilter.events.size > 0 || appliedFilter.filters.size > 0;
|
||||
|
||||
return (
|
||||
<OutsideClickDetectingDiv className={ stl.wrapper } onClickOutside={ this.closeModal } >
|
||||
<FilterModal
|
||||
close={ this.closeModal }
|
||||
displayed={ showFilterModal }
|
||||
loading={ loading }
|
||||
searchedEvents={ searchedEvents }
|
||||
searchQuery={ searchQuery }
|
||||
/>
|
||||
|
||||
{ hasFilters &&
|
||||
<div className={cn("bg-white rounded border-gray-light mt-2 relative", { 'blink-border' : blink })}>
|
||||
<div className="absolute right-0 top-0 m-3 z-10 flex items-center">
|
||||
<div className="mr-2">Operator</div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="condition"
|
||||
extraSmall={true}
|
||||
// className="my-3"
|
||||
onSelect={ this.changeConditionTab }
|
||||
value={{ value: appliedFilter.condition }}
|
||||
list={ [
|
||||
{ name: 'AND', value: 'and' },
|
||||
{ name: 'OR', value: 'or' },
|
||||
{ name: 'THEN', value: 'then' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ events.size > 0 &&
|
||||
<>
|
||||
<div className="py-1"><ListHeader title="Events" /></div>
|
||||
{ events.map((event, i) => (
|
||||
<EventEditor
|
||||
index={ i }
|
||||
key={ event._key }
|
||||
event={ event }
|
||||
onDNDMove={ this.props.moveEvent }
|
||||
/>
|
||||
)) }
|
||||
</>
|
||||
}
|
||||
<Attributes />
|
||||
<hr className="divider-light m-0 h-0"/>
|
||||
|
||||
<div className="bg-white flex items-center py-2" style={{ borderBottomLeftRadius: '3px', borderBottomRightRadius: '3px'}}>
|
||||
<div className="mr-auto ml-2">
|
||||
<CustomFilters
|
||||
buttonComponent={
|
||||
<div>
|
||||
<IconButton icon="plus" label="ADD STEP" primaryText />
|
||||
</div>
|
||||
}
|
||||
showFilters={ true }
|
||||
/>
|
||||
</div>
|
||||
<SaveFilterButton />
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<IconButton plain label="CLEAR STEPS" onClick={ this.clearEvents } />
|
||||
</div>
|
||||
<ManageFilters />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</OutsideClickDetectingDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import React from 'react';
|
||||
import { RandomElement } from 'UI';
|
||||
import stl from './randomPlaceholder.css';
|
||||
import Event, { TYPES } from 'Types/filter/event';
|
||||
import CustomFilter, { KEYS } from 'Types/filter/customFilter';
|
||||
|
||||
const getLabel = (type) => {
|
||||
if (type === KEYS.MISSING_RESOURCE) return 'Missing Resource';
|
||||
if (type === KEYS.SLOW_SESSION) return 'Slow Sessions';
|
||||
if (type === KEYS.USER_COUNTRY) return 'Country';
|
||||
if (type === KEYS.USER_BROWSER) return 'Browser';
|
||||
if (type === KEYS.USERID) return 'User Id';
|
||||
}
|
||||
|
||||
const getObject = (type, key) => {
|
||||
switch(type) {
|
||||
case TYPES.CLICK:
|
||||
case TYPES.INPUT:
|
||||
case TYPES.ERROR:
|
||||
case TYPES.LOCATION:
|
||||
return Event({ type, key: type });
|
||||
case KEYS.JOURNEY:
|
||||
return [
|
||||
Event({ type: TYPES.LOCATION, key: TYPES.LOCATION }),
|
||||
Event({ type: TYPES.LOCATION, key: TYPES.LOCATION }),
|
||||
Event({ type: TYPES.CLICK, key: TYPES.CLICK })
|
||||
]
|
||||
|
||||
case KEYS.USER_BROWSER:
|
||||
return CustomFilter({type, key: type, isFilter: true, label: getLabel(type), value: ['Chrome'] });
|
||||
case TYPES.METADATA:
|
||||
return CustomFilter({type, key, isFilter: true, label: key });
|
||||
case TYPES.USERID:
|
||||
return CustomFilter({type, key, isFilter: true, label: key });
|
||||
case KEYS.USER_COUNTRY:
|
||||
return CustomFilter({type, key: type, isFilter: true, value: ['FR'], label: getLabel(type) });
|
||||
case KEYS.SLOW_SESSION:
|
||||
case KEYS.MISSING_RESOURCE:
|
||||
return CustomFilter({type, key: type, hasNoValue: true, isFilter: true, label: getLabel(type) });
|
||||
}
|
||||
}
|
||||
|
||||
const getList = (onClick, appliedFilterKeys) => {
|
||||
let list = [
|
||||
{
|
||||
key: KEYS.CLICK,
|
||||
element: <div className={ stl.placeholder }>Find sessions with <span onClick={(e) => onClick(e, getObject(TYPES.CLICK))}>Click</span></div>
|
||||
},
|
||||
{
|
||||
key: KEYS.INPUT,
|
||||
element: <div className={ stl.placeholder }>Find sessions with <span onClick={(e) => onClick(e, getObject(TYPES.INPUT))}>Input</span></div>
|
||||
},
|
||||
{
|
||||
key: KEYS.ERROR,
|
||||
element: <div className={ stl.placeholder }>Find sessions with <span onClick={(e) => onClick(e, getObject(TYPES.ERROR))}>Errors</span></div>
|
||||
},
|
||||
{
|
||||
key: KEYS.LOCATION,
|
||||
element: <div className={ stl.placeholder }>Find sessions with <span onClick={(e) => onClick(e, getObject(TYPES.LOCATION))}>URL</span></div>
|
||||
},
|
||||
{
|
||||
key: TYPES.USERID,
|
||||
element: <div className={ stl.placeholder }>Find sessions with <span onClick={(e) => onClick(e, getObject(TYPES.USERID))}>User ID</span></div>
|
||||
},
|
||||
{
|
||||
key: KEYS.JOURNEY,
|
||||
element: <div className={ stl.placeholder }>Find sessions in a <span onClick={(e) => onClick(e, getObject(KEYS.JOURNEY))}>Journey</span></div>
|
||||
},
|
||||
{
|
||||
key: KEYS.USER_COUNTRY,
|
||||
element: <div className={ stl.placeholder }>Find sessions from <span onClick={(e) => onClick(e, getObject(KEYS.USER_COUNTRY))}>France</span></div>
|
||||
},
|
||||
{
|
||||
key: KEYS.USER_BROWSER,
|
||||
element: <div className={ stl.placeholder }>Find sessions on <span onClick={(e) => onClick(e, getObject(KEYS.USER_BROWSER))}>Chrome</span></div>
|
||||
},
|
||||
]
|
||||
|
||||
return list.filter(({key}) => !appliedFilterKeys.includes(key))
|
||||
}
|
||||
|
||||
const RandomPlaceholder = ({ onClick, appliedFilterKeys }) => {
|
||||
return (
|
||||
<RandomElement list={ getList(onClick, appliedFilterKeys) } />
|
||||
);
|
||||
};
|
||||
|
||||
export default RandomPlaceholder;
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import cn from 'classnames';
|
||||
import { TYPES } from 'Types/filter/event';
|
||||
import { LEVEL } from 'Types/session/log';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
import styles from './typeBadge.css';
|
||||
|
||||
function getText(type, source) {
|
||||
if (type === TYPES.CLICK) return 'Click';
|
||||
if (type === TYPES.LOCATION) return 'URL';
|
||||
if (type === TYPES.VIEW) return 'View';
|
||||
if (type === TYPES.INPUT) return 'Input';
|
||||
if (type === TYPES.CONSOLE) return 'Console';
|
||||
if (type === TYPES.GRAPHQL) return 'GraphQL';
|
||||
if (type === TYPES.ERROR) return 'Error';
|
||||
if (type === TYPES.STATEACTION) return 'Store Action';
|
||||
if (type === TYPES.FETCH) return 'Fetch';
|
||||
if (type === TYPES.REVID) return 'Rev ID';
|
||||
if (type === TYPES.METADATA) return 'Metadata';
|
||||
if (type === TYPES.CUSTOM) {
|
||||
if (!source) return 'Custom';
|
||||
return (
|
||||
<React.Fragment >
|
||||
<Icon name={ `integrations/${ source }` } size="12" inline className={ cn(styles.icon, "mr-5") } />
|
||||
{ 'Custom' }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return '?';
|
||||
}
|
||||
|
||||
const TypeBadge = ({ event: { type, level, source } }) => (
|
||||
<div
|
||||
className={ cn(styles.badge, {
|
||||
[ styles.red ]: level === LEVEL.ERROR || level === LEVEL.EXCEPTION,
|
||||
[ styles.yellow ]: level === LEVEL.WARN,
|
||||
}) }
|
||||
>
|
||||
{ getText(type, source) }
|
||||
</div>
|
||||
);
|
||||
|
||||
TypeBadge.displayName = 'TypeBadge';
|
||||
|
||||
export default TypeBadge;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
.eventDropdownItem {
|
||||
padding: 8px 0;
|
||||
padding-left: 18px;
|
||||
border-bottom: solid thin $gray-light;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: solid thin transparent;
|
||||
}
|
||||
|
||||
& .values {
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.inputType,
|
||||
&.clickType {
|
||||
color: $gray-darkest !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.consoleType {
|
||||
font-family: 'menlo', 'monaco', 'consolas', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
@import 'mixins.css';
|
||||
|
||||
@import 'icons.css';
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 8px 15px;
|
||||
background-color: white;
|
||||
border-bottom: solid thin $gray-lightest;
|
||||
transition: all 0.4s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: solid thin transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
transition: all 0.2s;
|
||||
|
||||
& .actions {
|
||||
opacity: 1;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
& .leftSection,
|
||||
& .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& .leftSection {
|
||||
flex: 1;
|
||||
& > div {
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.index {
|
||||
background: $white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
margin-right: 10px;
|
||||
color: $gray-medium;
|
||||
font-weight: 300;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) inset;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
opacity: 0;
|
||||
transition: all 0.4s;
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
.searchField {
|
||||
box-shadow: none !important;
|
||||
& input {
|
||||
box-shadow: none !important;
|
||||
border-radius: 3 !important;
|
||||
border: solid thin $gray-light !important;
|
||||
height: 46px !important;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-shadow: none !important;
|
||||
position: relative;
|
||||
|
||||
& .clearStepsButton {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10x;
|
||||
}
|
||||
}
|
||||
|
||||
.randomElement {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 8;
|
||||
padding: 15px;
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.dropdownMenu {
|
||||
max-width: 100%;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
|
||||
&[data-hidden=true] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.header {
|
||||
padding: 5px 10px;
|
||||
letter-spacing: 1.5px;
|
||||
background-color: $gray-lightest;
|
||||
color: $gray-medium;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dateRange {
|
||||
color: red;
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: $gray-medium;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
|
||||
& span {
|
||||
font-weight: 400;
|
||||
color: $teal;
|
||||
cursor: pointer;
|
||||
border-bottom: dashed thin $teal;
|
||||
|
||||
&:hover {
|
||||
color: $teal-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './EventFilter';
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
.placeholder {
|
||||
color: $gray-medium;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
|
||||
& span {
|
||||
font-weight: 400;
|
||||
color: $teal;
|
||||
cursor: pointer;
|
||||
border-bottom: dashed thin $teal;
|
||||
|
||||
&:hover {
|
||||
color: $teal-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
.badge {
|
||||
font-size: 11px;
|
||||
border-radius: 3px;
|
||||
background-color: white;
|
||||
border: solid thin $gray-light;
|
||||
padding: 2px 0;
|
||||
text-align: center;
|
||||
width: 66px;
|
||||
margin-right: 10px;
|
||||
user-select: none;
|
||||
|
||||
&.red {
|
||||
background-color: rgba(204, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&.yellow {
|
||||
background-color: rgba(245, 166, 35, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import stl from './filterSelectionButton.css';
|
||||
import stl from './filterSelectionButton.module.css';
|
||||
|
||||
const FilterSelectionButton = ({ label }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dropdown } from 'semantic-ui-react';
|
||||
import Select from 'Shared/Select';
|
||||
import { Icon } from 'UI';
|
||||
import { sort } from 'Duck/sessions';
|
||||
import { applyFilter } from 'Duck/search';
|
||||
import stl from './sortDropdown.css';
|
||||
import stl from './sortDropdown.module.css';
|
||||
|
||||
@connect(null, { sort, applyFilter })
|
||||
export default class SortDropdown extends React.PureComponent {
|
||||
state = { value: null }
|
||||
sort = (e, { value }) => {
|
||||
sort = ({ value }) => {
|
||||
this.setState({ value: value })
|
||||
const [ sort, order ] = value.split('-');
|
||||
const sign = order === 'desc' ? -1 : 1;
|
||||
|
|
@ -21,14 +22,14 @@ export default class SortDropdown extends React.PureComponent {
|
|||
render() {
|
||||
const { options } = this.props;
|
||||
return (
|
||||
<Dropdown
|
||||
<Select
|
||||
name="sortSessions"
|
||||
className={ stl.dropdown }
|
||||
direction="left"
|
||||
plain
|
||||
// className={ stl.dropdown }
|
||||
right
|
||||
options={ options }
|
||||
onChange={ this.sort }
|
||||
defaultValue={ options[ 0 ].value }
|
||||
icon={null}
|
||||
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { Button } from 'UI';
|
||||
import { applyFilter } from 'Duck/filters';
|
||||
import styles from './findBlock.css';
|
||||
|
||||
@connect(state => ({
|
||||
eventsCount: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size,
|
||||
lodaing: state.getIn([ 'sessions', 'loading' ]),
|
||||
}), {
|
||||
applyFilter,
|
||||
})
|
||||
export default class FindBlock extends React.PureComponent {
|
||||
onClick = () => this.props.applyFilter()
|
||||
render() {
|
||||
const { lodaing, eventsCount } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ styles.findBlock }>
|
||||
<div>
|
||||
<Button
|
||||
className={ styles.findButton }
|
||||
onClick={ this.onClick }
|
||||
primary
|
||||
loading={ lodaing }
|
||||
disabled={ eventsCount === 0 }
|
||||
>
|
||||
{'Find Sessions'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import styles from './insights.css';
|
||||
import styles from './insights.module.css';
|
||||
|
||||
const Insights = ({ insights }) => (
|
||||
<div className={ styles.notes }>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import stl from './listHeader.css';
|
||||
import stl from './listHeader.module.css';
|
||||
|
||||
const ListHeader = ({ title }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import { Button } from 'UI';
|
||||
import styles from './activeFilterDetails.css';
|
||||
import cn from 'classnames';
|
||||
import { BrowserIcon, OsIcon } from 'UI';
|
||||
import TypeBadge from '../EventFilter/TypeBadge';
|
||||
|
||||
export default ({
|
||||
activeFilter, applyFiltersHandler, removeFilter, loading,
|
||||
}) => (
|
||||
<div className={ styles.filterDetails }>
|
||||
<div className={ styles.title }>
|
||||
{ activeFilter.name }
|
||||
</div>
|
||||
<div>
|
||||
<div className={ styles.userEvents }>
|
||||
<div className={ styles.filterLabel }>{ 'User Events' }</div>
|
||||
<div className={ styles.list }>
|
||||
<div>
|
||||
{ activeFilter.events.map((item, i) => (
|
||||
<div className={ styles.filterType }>
|
||||
<div className={ styles.indexCount }>{ i+1 }</div>
|
||||
<TypeBadge event={ item } />
|
||||
<div className={ styles.value }>{ item.value }</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={ styles.filterType } data-hidden={ !activeFilter.userCountry }>
|
||||
<div className={ styles.filterLabel }>{ 'Location:' }</div>
|
||||
<div>
|
||||
<span className={ styles.badge }>{ activeFilter.userCountry }</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={ styles.filterType } data-hidden={ !activeFilter.userBrowser }>
|
||||
<div className={ styles.filterLabel }>{ 'Browser:' }</div>
|
||||
<div className={ cn('flex items-center', styles.badge) }>
|
||||
<BrowserIcon browser={ activeFilter.userBrowser || '' } size="16" className="mr-5" />
|
||||
<span >{ activeFilter.userBrowser }</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={ styles.filterType } data-hidden={ !activeFilter.userOs }>
|
||||
<div className={ styles.filterLabel }>{ 'OS:' }</div>
|
||||
<div className={ cn('flex items-center', styles.badge) }>
|
||||
<OsIcon os={ activeFilter.userOs || '' } size="16" className="mr-5" />
|
||||
<span >{ activeFilter.userOs }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ styles.footer }>
|
||||
<Button primary marginRight onClick={ () => applyFiltersHandler(activeFilter) }>{ 'Apply' }</Button>
|
||||
<Button
|
||||
onClick={ () => removeFilter(activeFilter.id) }
|
||||
basic
|
||||
loading={ loading }
|
||||
>
|
||||
{ 'Delete' }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { IconButton } from 'UI';
|
||||
import Funnel from 'Types/funnel';
|
||||
import {
|
||||
remove as removeFilter,
|
||||
setActive as setActiveFilter,
|
||||
applyFilter,
|
||||
toggleFilterModal
|
||||
} from 'Duck/filters';
|
||||
import {
|
||||
fetchList as fetchFilterList,
|
||||
save as saveFunnel
|
||||
} from 'Duck/funnels';
|
||||
import withToggle from 'Components/hocs/withToggle';
|
||||
import SaveModal from './SaveModal';
|
||||
|
||||
@withToggle('slideModalDisplayed', 'toggleSlideModal')
|
||||
@connect(
|
||||
state =>
|
||||
({
|
||||
savedFilters: state.getIn([ 'filters', 'list' ]),
|
||||
activeFilter: state.getIn([ 'filters', 'activeFilter' ]),
|
||||
fetching: state.getIn([ 'filters', 'fetchListRequest', 'loading' ]),
|
||||
loading: state.getIn([ 'filters', 'loading' ]),
|
||||
saveModalOpen: state.getIn([ 'filters', 'saveModalOpen' ]),
|
||||
appliedFilter: state.getIn([ 'filters', 'appliedFilter' ]),
|
||||
customFilters: state.getIn([ 'filters', 'customFilters']),
|
||||
})
|
||||
,
|
||||
{
|
||||
fetchFilterList,
|
||||
saveFunnel,
|
||||
removeFilter,
|
||||
setActiveFilter,
|
||||
applyFilter,
|
||||
toggleFilterModal,
|
||||
},
|
||||
)
|
||||
export default class ManageFilters extends React.PureComponent {
|
||||
updateFilter = (name, isPublic = false) => {
|
||||
const { appliedFilter } = this.props;
|
||||
const savedFilter = Funnel({name, filter: appliedFilter, isPublic });
|
||||
this.props.saveFunnel(savedFilter).then(function() {
|
||||
this.props.fetchFilterList();
|
||||
this.props.toggleFilterModal(false);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
applyFiltersHandler = (filter) => {
|
||||
this.props.applyFilter(filter);
|
||||
this.props.toggleSlideModal(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
saveModalOpen,
|
||||
appliedFilter,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
primaryText
|
||||
className="mr-2"
|
||||
label="SAVE FUNNEL"
|
||||
onClick={ () => this.props.toggleFilterModal(true) }
|
||||
/>
|
||||
<SaveModal
|
||||
saveModalOpen={ saveModalOpen }
|
||||
appliedFilter={ appliedFilter }
|
||||
toggleFilterModal={ this.props.toggleFilterModal }
|
||||
updateFilter={ this.updateFilter }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { Button, Modal, Form, Icon, Checkbox } from 'UI';
|
||||
import styles from './saveModal.css';
|
||||
|
||||
@connect(state => ({
|
||||
loading: state.getIn([ 'funnels', 'saveRequest', 'loading' ]) || state.getIn([ 'funnels', 'updateRequest', 'loading' ]),
|
||||
}))
|
||||
export default class SaveModal extends React.PureComponent {
|
||||
state = { name: 'Untitled', isPublic: false };
|
||||
static getDerivedStateFromProps(props) {
|
||||
if (!props.saveModalOpen) {
|
||||
return {
|
||||
name: props.appliedFilter.name || 'Untitled',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onNameChange = ({ target: { value } }) => {
|
||||
this.setState({ name: value });
|
||||
};
|
||||
|
||||
onChangeOption = (e, { checked, name }) => this.setState({ [ name ]: !this.state.isPublic })
|
||||
|
||||
onSave = () => {
|
||||
const { toggleFilterModal } = this.props;
|
||||
const { name, isPublic } = this.state;
|
||||
if (name.trim() === '') return;
|
||||
this.props.updateFilter(name.trim(), isPublic);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
saveModalOpen,
|
||||
appliedFilter,
|
||||
toggleFilterModal,
|
||||
loading,
|
||||
} = this.props;
|
||||
const { name, isPublic } = this.state;
|
||||
|
||||
return (
|
||||
<Modal size="tiny" open={ saveModalOpen }>
|
||||
<Modal.Header className={ styles.modalHeader }>
|
||||
<div>{ 'Save Funnel' }</div>
|
||||
<Icon
|
||||
role="button"
|
||||
tabIndex="-1"
|
||||
color="gray-dark"
|
||||
size="14"
|
||||
name="close"
|
||||
onClick={ () => toggleFilterModal(false) }
|
||||
/>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Content>
|
||||
<Form onSubmit={this.onSave}>
|
||||
<Form.Field>
|
||||
<label>{'Title:'}</label>
|
||||
<input
|
||||
autoFocus={ true }
|
||||
className={ styles.name }
|
||||
name="name"
|
||||
value={ name }
|
||||
onChange={ this.onNameChange }
|
||||
placeholder="Title"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
name="isPublic"
|
||||
className="font-medium"
|
||||
type="checkbox"
|
||||
checked={ isPublic }
|
||||
onClick={ () => this.setState({ 'isPublic' : !isPublic }) }
|
||||
className="mr-3"
|
||||
/>
|
||||
<div className="flex items-center cursor-pointer" onClick={ () => this.setState({ 'isPublic' : !isPublic }) }>
|
||||
<Icon name="user-friends" size="16" />
|
||||
<span className="ml-2"> Team Visible</span>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Actions className="">
|
||||
<Button
|
||||
primary
|
||||
onClick={ this.onSave }
|
||||
loading={ loading }
|
||||
>
|
||||
{ appliedFilter.filterId ? 'Modify' : 'Save' }
|
||||
</Button>
|
||||
<Button className={ styles.cancelButton } marginRight onClick={ () => toggleFilterModal(false) }>{ 'Cancel' }</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import styles from './savedFilterList.css';
|
||||
|
||||
export default ({ savedFilters, activeFilter, onFilterClick }) => (
|
||||
<div className={ styles.filtersContainer }>
|
||||
{ savedFilters && savedFilters.size > 0 &&
|
||||
savedFilters.map((filter, index) => filter &&
|
||||
<div
|
||||
className={ styles.filter }
|
||||
data-active={ activeFilter && filter.id === activeFilter.id }
|
||||
onClick={ () => onFilterClick(filter) }
|
||||
key={ index }
|
||||
>
|
||||
{ filter.name }
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
.userEvents {
|
||||
& .list {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: solid thin $gray-light;
|
||||
border-radius: 3px;
|
||||
background-color: white;
|
||||
& .filterType {
|
||||
border-bottom: solid thin $gray-light;
|
||||
padding: 8px 10px;
|
||||
align-items: center;
|
||||
|
||||
& .value {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
& .indexCount {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
box-shadow: 0 1px 5px 0 $gray-light;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filterDetails {
|
||||
width: 400px;
|
||||
padding: 20px;
|
||||
|
||||
& .title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterType {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
padding: 10px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.filterLabel {
|
||||
font-weight: bold;
|
||||
width: 100px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eventsBadge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 5px 10px;
|
||||
background-color: $gray-light;
|
||||
margin-right: 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
/* margin-bottom: 10px; */
|
||||
}
|
||||
|
||||
[data-hidden=true] {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ManageFilters';
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
.filter {
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
border-top: solid thin $gray-light;
|
||||
border-bottom: solid thin transparent;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: solid thin $gray-light;
|
||||
}
|
||||
|
||||
&[data-active=true],
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Input, Slider, Button, Popup, CircularLoader } from 'UI';
|
||||
import { saveCaptureRate, editCaptureRate } from 'Duck/watchdogs';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './sessionCaptureRate.css';
|
||||
|
||||
function isPercent(val) {
|
||||
if (isNaN(+val)) return false;
|
||||
if (+val > 100 || +val < 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const SessionCaptureRate = props => {
|
||||
const { captureRate, saveCaptureRate, editCaptureRate, loading, onClose } = props;
|
||||
const _sampleRate = captureRate.get('rate');
|
||||
if (_sampleRate == null) return null;
|
||||
|
||||
const [sampleRate, setSampleRate] = useState(_sampleRate)
|
||||
|
||||
const captureAll = captureRate.get('captureAll');
|
||||
|
||||
const onSampleRateChange = (e) => {
|
||||
saveCaptureRate({ rate: sampleRate, captureAll: captureAll }).then(onClose);
|
||||
}
|
||||
const onCaptureAllChange = () => saveCaptureRate({ rate: sampleRate, captureAll: !captureAll });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popup
|
||||
trigger={
|
||||
<Slider
|
||||
name="sessionsLive"
|
||||
onChange={ onCaptureAllChange }
|
||||
checked={ captureAll }
|
||||
className={stl.customSlider}
|
||||
label="Capture All"
|
||||
/>
|
||||
}
|
||||
content={ `Capture All` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
/>
|
||||
{ !captureAll && (
|
||||
<div className="flex items-center justify-between mt-4 border-t pt-4">
|
||||
<Input
|
||||
icon="percent"
|
||||
name="sampleRate"
|
||||
disabled={ captureAll }
|
||||
value={ captureAll ? '100' : sampleRate }
|
||||
onChange={ ({ target: { value }}) => isPercent(value) && setSampleRate(+value) }
|
||||
size="small"
|
||||
className={stl.inputField}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
primary
|
||||
onClick={onSampleRateChange}
|
||||
disabled={loading}
|
||||
>
|
||||
<CircularLoader loading={ loading } style={ { marginRight: '8px' } } />
|
||||
Apply
|
||||
</Button>
|
||||
<Button outline onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
currentProjectId: state.getIn([ 'site', 'siteId' ]),
|
||||
captureRate: state.getIn(['watchdogs', 'captureRate']),
|
||||
loading: state.getIn(['watchdogs', 'savingCaptureRate', 'loading']),
|
||||
}), {
|
||||
saveCaptureRate, editCaptureRate
|
||||
})(SessionCaptureRate);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SessionCaptureRate';
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
.inputField {
|
||||
max-width: 140px !important;
|
||||
& label {
|
||||
font-weight: 300 !important;
|
||||
}
|
||||
& input {
|
||||
max-width: 70px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.customSlider {
|
||||
line-height: 20px !important;
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import SessionStack from 'Shared/SessionStack/SessionStack'
|
||||
import FunnelListHeader from 'Components/Funnels/FunnelListHeader';
|
||||
|
||||
function SessionFlowList({ activeTab, savedFilters, loading }) {
|
||||
return (
|
||||
<div>
|
||||
<FunnelListHeader activeTab={activeTab} count={0} />
|
||||
<NoContent
|
||||
title="No Flows Found!"
|
||||
subtext="Please try changing your search parameters."
|
||||
animatedIcon="no-results"
|
||||
show={ !loading && savedFilters.size === 0 }
|
||||
>
|
||||
<Loader loading={ loading }>
|
||||
{savedFilters.map(item => (
|
||||
<div className="mb-4" key={item.key}>
|
||||
<SessionStack flow={item} />
|
||||
</div>
|
||||
))}
|
||||
</Loader>
|
||||
</NoContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
loading: state.getIn([ 'filters', 'fetchListRequest', 'loading' ]),
|
||||
activeTab: state.getIn([ 'sessions', 'activeTab' ]),
|
||||
savedFilters: state.getIn([ 'filters', 'list' ]),
|
||||
}), {})(SessionFlowList)
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SessionFlowList'
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Loader, NoContent, Button, Pagination } from 'UI';
|
||||
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;
|
||||
|
|
@ -94,24 +96,16 @@ export default class SessionList extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<NoContent
|
||||
title={this.getNoContentMessage(activeTab)}
|
||||
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"
|
||||
// animatedIcon="no-results"
|
||||
show={ !loading && list.size === 0}
|
||||
subtext={
|
||||
<div>
|
||||
<div>Please try changing your search parameters.</div>
|
||||
{/* {allList.size > 0 && (
|
||||
<div className="pt-2">
|
||||
However, we found other sessions based on your search parameters.
|
||||
<div>
|
||||
<Button
|
||||
plain
|
||||
onClick={() => onMenuItemClick({ name: 'All', type: 'all' })}
|
||||
>See All</Button>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -142,18 +136,6 @@ export default class SessionList extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { activeTab, allList, total } = this.props;
|
||||
// var filteredList;
|
||||
|
||||
// if (activeTab.type !== ALL && activeTab.type !== 'bookmark' && activeTab.type !== 'live') { // Watchdog sessions
|
||||
// filteredList = allList.filter(session => activeTab.fits(session))
|
||||
// } else {
|
||||
// filteredList = allList
|
||||
// }
|
||||
|
||||
// if (activeTab.type === 'bookmark') {
|
||||
// filteredList = filteredList.filter(item => item.favorite)
|
||||
// }
|
||||
// const _total = activeTab.type === 'all' ? total : allList.size
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { Button } from 'UI';
|
||||
import styles from './sessionListFooter.css';
|
||||
import styles from './sessionListFooter.module.css';
|
||||
|
||||
const SessionListFooter = ({
|
||||
displayedCount, totalCount, loading, onLoadMoreClick,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { applyFilter } from 'Duck/filters';
|
||||
import SortDropdown from '../Filters/SortDropdown';
|
||||
import DateRange from '../DateRange';
|
||||
import { TimezoneDropdown } from 'UI';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import DropdownPlain from 'Shared/DropdownPlain';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { applyFilter } from 'Duck/search';
|
||||
import Period from 'Types/app/period';
|
||||
|
||||
const DEFAULT_SORT = 'startTs';
|
||||
const DEFAULT_ORDER = 'desc';
|
||||
const sortOptionsMap = {
|
||||
'startTs-desc': 'Newest',
|
||||
'startTs-asc': 'Oldest',
|
||||
|
|
@ -16,16 +13,22 @@ const sortOptionsMap = {
|
|||
'eventsCount-desc': 'Events Descending',
|
||||
};
|
||||
const sortOptions = Object.entries(sortOptionsMap)
|
||||
.map(([ value, text ]) => ({ value, text }));
|
||||
.map(([ value, label ]) => ({ value, label }));
|
||||
|
||||
|
||||
function SessionListHeader({
|
||||
activeTab,
|
||||
count,
|
||||
applyFilter,
|
||||
...props
|
||||
filter,
|
||||
}) {
|
||||
// useEffect(() => { applyFilter({ sort: DEFAULT_SORT, order: DEFAULT_ORDER }) }, [])
|
||||
const { startDate, endDate, rangeValue } = filter;
|
||||
const period = new Period({ start: startDate, end: endDate, rangeName: rangeValue });
|
||||
|
||||
const onDateChange = (e) => {
|
||||
const dateValues = e.toJSON();
|
||||
applyFilter(dateValues);
|
||||
};
|
||||
return (
|
||||
<div className="flex mb-6 justify-between items-end">
|
||||
<div className="flex items-baseline">
|
||||
|
|
@ -36,22 +39,14 @@ function SessionListHeader({
|
|||
{ activeTab.type !== 'bookmark' && (
|
||||
<div className="ml-3 flex items-center">
|
||||
<span className="mr-2 color-gray-medium">Sessions Captured in</span>
|
||||
<DateRange />
|
||||
<SelectDateRange
|
||||
period={period}
|
||||
onChange={onDateChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{/* <div className="flex items-center">
|
||||
<span className="mr-2 color-gray-medium">Session View</span>
|
||||
<DropdownPlain
|
||||
options={[
|
||||
{ text: 'List', value: 'list' },
|
||||
{ text: 'Grouped', value: 'grouped' }
|
||||
]}
|
||||
onChange={() => {}}
|
||||
value='list'
|
||||
/>
|
||||
</div> */}
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Sort By</span>
|
||||
<SortDropdown options={ sortOptions }/>
|
||||
|
|
@ -63,4 +58,6 @@ function SessionListHeader({
|
|||
|
||||
export default connect(state => ({
|
||||
activeTab: state.getIn([ 'search', 'activeTab' ]),
|
||||
period: state.getIn([ 'search', 'period' ]),
|
||||
filter: state.getIn([ 'search', 'instance' ]),
|
||||
}), { applyFilter })(SessionListHeader);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
|
||||
export default class Tooltip extends React.PureComponent {
|
||||
|
|
@ -26,15 +27,14 @@ export default class Tooltip extends React.PureComponent {
|
|||
open={ open }
|
||||
content={ tooltip }
|
||||
inverted
|
||||
trigger={
|
||||
<span
|
||||
onMouseEnter={ this.onMouseEnter }
|
||||
onMouseLeave={ this.onMouseLeave }
|
||||
>
|
||||
{ trigger }
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<span
|
||||
onMouseEnter={ this.onMouseEnter }
|
||||
onMouseLeave={ this.onMouseLeave }
|
||||
>
|
||||
{ trigger }
|
||||
</span>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux';
|
||||
import { Tooltip } from 'react-tippy'
|
||||
import cn from 'classnames';
|
||||
import { SideMenuitem, SavedSearchList, Progress, Popup } from 'UI'
|
||||
import stl from './sessionMenu.css';
|
||||
import { SideMenuitem, SavedSearchList, Popup } 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';
|
||||
|
|
@ -25,32 +24,13 @@ function SessionsMenu(props) {
|
|||
<span>Sessions</span>
|
||||
</div>
|
||||
<span className={ cn(stl.manageButton, 'mr-2') } onClick={() => showModal(<SessionSettings />, { right: true })}>
|
||||
<Tooltip
|
||||
<Popup
|
||||
hideOnClick={true}
|
||||
position="bottom"
|
||||
size="small"
|
||||
html={<span>Configure the percentage of sessions <br /> to be captured, timezone and more.</span>}
|
||||
content={<span>Configure the percentage of sessions <br /> to be captured, timezone and more.</span>}
|
||||
>
|
||||
Settings
|
||||
</Tooltip>
|
||||
</Popup>
|
||||
</span>
|
||||
{/* { !capturingAll && (
|
||||
<Popup
|
||||
trigger={
|
||||
<div
|
||||
style={{ width: '120px' }}
|
||||
className="ml-6 cursor-pointer"
|
||||
onClick={ toggleRehydratePanel }
|
||||
>
|
||||
<Progress success percent={ props.captureRate.get('rate') } indicating size="tiny" />
|
||||
</div>
|
||||
}
|
||||
content={ `Capturing ${props.captureRate.get('rate')}% of all sessions. Click to manage capture rate. ` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top right"
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue