feat(ui) - session settings - capture rate api update
This commit is contained in:
parent
87f76f484d
commit
18e932e5e9
25 changed files with 366 additions and 74 deletions
|
|
@ -20,9 +20,9 @@ function SessionsMenu(props) {
|
|||
|
||||
const capturingAll = props.captureRate && props.captureRate.get('captureAll');
|
||||
|
||||
useEffect(() => {
|
||||
showModal(<SessionSettings />, {});
|
||||
}, [])
|
||||
// useEffect(() => {
|
||||
// showModal(<SessionSettings />, {});
|
||||
// }, [])
|
||||
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
|
|
@ -30,8 +30,8 @@ function SessionsMenu(props) {
|
|||
<div className={ stl.label }>
|
||||
<span>Sessions</span>
|
||||
</div>
|
||||
{capturingAll && <span className={ cn(stl.manageButton, 'mr-2') } onClick={ toggleRehydratePanel }>Manage</span>}
|
||||
{ !capturingAll && (
|
||||
<span className={ cn(stl.manageButton, 'mr-2') } onClick={() => showModal(<SessionSettings />, { right: true })}>Manage</span>
|
||||
{/* { !capturingAll && (
|
||||
<Popup
|
||||
trigger={
|
||||
<div
|
||||
|
|
@ -47,8 +47,11 @@ function SessionsMenu(props) {
|
|||
inverted
|
||||
position="top right"
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
{/* <div className="text-sm color-gray-medium cursor-pointer mb-4" style={{ textDecoration: 'underline dotted'}} onClick={() => showModal(<SessionSettings />, {})}>
|
||||
Capture, Listing, and Timezone Settings
|
||||
</div> */}
|
||||
|
||||
<div>
|
||||
<SideMenuitem
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ export default class NewSiteForm extends React.PureComponent {
|
|||
const { sites } = this.props;
|
||||
const site = sites.last();
|
||||
if (!pathname.includes('/client')) {
|
||||
console.log('site', site)
|
||||
this.props.setSiteId(site.get('id'))
|
||||
}
|
||||
this.props.onClose(null, site)
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import { useModal } from '.';
|
|||
import ModalOverlay from './ModalOverlay';
|
||||
|
||||
export default function Modal({ children }){
|
||||
const { component } = useModal();
|
||||
const { component, props} = useModal();
|
||||
|
||||
return component ? ReactDOM.createPortal(
|
||||
<ModalOverlay>
|
||||
<ModalOverlay left={!props.right} right={props.right}>
|
||||
{component}
|
||||
</ModalOverlay>,
|
||||
document.querySelector("#modal-root"),
|
||||
|
|
|
|||
|
|
@ -7,13 +7,26 @@
|
|||
/* transition: all 0.3s ease-in-out; */
|
||||
animation: fade 1s forwards;
|
||||
}
|
||||
|
||||
.slide {
|
||||
position: absolute;
|
||||
/* left: -100%; */
|
||||
/* -webkit-animation: slide 0.5s forwards;
|
||||
animation: slide 0.5s forwards; */
|
||||
}
|
||||
|
||||
.slideLeft {
|
||||
left: -100%;
|
||||
-webkit-animation: slide 0.5s forwards;
|
||||
animation: slide 0.5s forwards;
|
||||
}
|
||||
|
||||
.slideRight {
|
||||
right: -100%;
|
||||
-webkit-animation: slideRight 0.5s forwards;
|
||||
animation: slideRight 0.5s forwards;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
|
@ -29,4 +42,12 @@
|
|||
|
||||
@keyframes slide {
|
||||
100% { left: 0; }
|
||||
}
|
||||
|
||||
@-webkit-keyframes slideRight {
|
||||
100% { right: 0; }
|
||||
}
|
||||
|
||||
@keyframes slideRight {
|
||||
100% { right: 0%; }
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import stl from './ModalOverlay.css'
|
||||
import cn from 'classnames';
|
||||
|
||||
function ModalOverlay({ children }) {
|
||||
function ModalOverlay({ children, left = false, right = false }) {
|
||||
let modal = useModal();
|
||||
|
||||
return (
|
||||
|
|
@ -12,7 +13,7 @@ function ModalOverlay({ children }) {
|
|||
className={stl.overlay}
|
||||
style={{ background: "rgba(0,0,0,0.5)" }}
|
||||
/>
|
||||
<div className={stl.slide}>{children}</div>
|
||||
<div className={cn(stl.slide, { [stl.slideLeft] : left, [stl.slideRight] : right })}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import Modal from './Modal';
|
|||
|
||||
const ModalContext = createContext({
|
||||
component: null,
|
||||
props: {},
|
||||
props: {
|
||||
right: false,
|
||||
},
|
||||
showModal: (component: any, props: any) => {},
|
||||
hideModal: () => {}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -276,13 +276,13 @@ export default class Controls extends React.Component {
|
|||
label="Back"
|
||||
icon="replay-10"
|
||||
/>
|
||||
<ControlButton
|
||||
{/* <ControlButton
|
||||
disabled={ disabled }
|
||||
onClick={ this.props.toggleSkipToIssue }
|
||||
active={ skipToIssue }
|
||||
label="Skip to Issue"
|
||||
icon={skipToIssue ? 'skip-forward-fill' : 'skip-forward'}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import Select from 'react-select';
|
|||
interface Props {
|
||||
options: any[];
|
||||
isSearchable?: boolean;
|
||||
defaultValue?: any;
|
||||
defaultValue?: string;
|
||||
plain?: boolean;
|
||||
[x:string]: any;
|
||||
}
|
||||
export default function({ plain = false, options, isSearchable = false, defaultValue, ...rest }: Props) {
|
||||
export default function({ plain = false, options, isSearchable = false, defaultValue = '', ...rest }: Props) {
|
||||
const customStyles = {
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
|
|
@ -39,11 +39,12 @@ export default function({ plain = false, options, isSearchable = false, defaultV
|
|||
return { ...provided, opacity, transition };
|
||||
}
|
||||
}
|
||||
const defaultSelected = defaultValue ? options.find(x => x.value === defaultValue) : options[0];
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
isSearchable={isSearchable}
|
||||
defaultValue={defaultValue ? options.find(i => i.value === defaultValue) : options[0]}
|
||||
defaultValue={defaultSelected}
|
||||
components={{
|
||||
IndicatorSeparator: () => null
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,6 @@ const ASSIST_ROUTE = assistRoute();
|
|||
const ASSIST_LIVE_SESSION = liveSession()
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
// const Label = ({ label = '', color = 'color-gray-medium'}) => (
|
||||
// <div className={ cn('font-light text-sm', color)}>{label}</div>
|
||||
// )
|
||||
@connect(state => ({
|
||||
timezone: state.getIn(['sessions', 'timezone']),
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
|
|
|
|||
164
frontend/app/components/shared/SessionItem/SessionItem.tsx
Normal file
164
frontend/app/components/shared/SessionItem/SessionItem.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import {
|
||||
Link,
|
||||
Icon,
|
||||
CountryFlag,
|
||||
Avatar,
|
||||
TextEllipsis,
|
||||
Label,
|
||||
} from 'UI';
|
||||
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
|
||||
import { session as sessionRoute, liveSession as liveSessionRoute, withSiteId } from 'App/routes';
|
||||
import { durationFormatted, formatTimeOrDate } from 'App/date';
|
||||
import stl from './sessionItem.css';
|
||||
import Counter from './Counter'
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import SessionMetaList from './SessionMetaList';
|
||||
import ErrorBars from './ErrorBars';
|
||||
import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from "App/routes";
|
||||
import { capitalize } from 'App/utils';
|
||||
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER } from 'App/constants/storageKeys'
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
const ASSIST_LIVE_SESSION = liveSession()
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
// @connect(state => ({
|
||||
// timezone: state.getIn(['sessions', 'timezone']),
|
||||
// siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
// }), { toggleFavorite, setSessionPath })
|
||||
// @withRouter
|
||||
function SessionItem(props) {
|
||||
// render() {
|
||||
const {
|
||||
session: {
|
||||
sessionId,
|
||||
userBrowser,
|
||||
userOs,
|
||||
userId,
|
||||
userAnonymousId,
|
||||
userDisplayName,
|
||||
userCountry,
|
||||
startedAt,
|
||||
duration,
|
||||
eventsCount,
|
||||
errorsCount,
|
||||
pagesCount,
|
||||
viewed,
|
||||
favorite,
|
||||
userDeviceType,
|
||||
userUuid,
|
||||
userNumericHash,
|
||||
live,
|
||||
metadata,
|
||||
userSessionsCount,
|
||||
issueTypes,
|
||||
active,
|
||||
},
|
||||
timezone,
|
||||
onUserClick = () => null,
|
||||
hasUserFilter = false,
|
||||
disableUser = false,
|
||||
metaList = [],
|
||||
showActive = false,
|
||||
lastPlayedSessionId,
|
||||
} = props;
|
||||
const formattedDuration = durationFormatted(duration);
|
||||
const hasUserId = userId || userAnonymousId;
|
||||
const isSessions = isRoute(SESSIONS_ROUTE, props.location.pathname);
|
||||
const isAssist = isRoute(ASSIST_ROUTE, props.location.pathname) || isRoute(ASSIST_LIVE_SESSION, props.location.pathname);
|
||||
const isLastPlayed = lastPlayedSessionId === sessionId;
|
||||
|
||||
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
|
||||
const value = metadata[key];
|
||||
return { label: key, value };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ cn(stl.sessionItem, "flex flex-col bg-white p-3 mb-3") } id="session-item" >
|
||||
<div className="flex items-start">
|
||||
<div className={ cn('flex items-center w-full')}>
|
||||
<div className="flex items-center pr-2" style={{ width: "30%"}}>
|
||||
<div><Avatar seed={ userNumericHash } isAssist={isAssist} /></div>
|
||||
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center">
|
||||
<div
|
||||
className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
|
||||
onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
|
||||
>
|
||||
<TextEllipsis text={userDisplayName} maxWidth={200} popupProps={{ inverted: true, size: 'tiny' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "20%", height: "38px" }} className="px-2 flex flex-col justify-between">
|
||||
<div>{formatTimeOrDate(startedAt, timezone) }</div>
|
||||
<div className="flex items-center color-gray-medium">
|
||||
{!isAssist && (
|
||||
<>
|
||||
<div className="color-gray-medium">
|
||||
<span className="mr-1">{ eventsCount }</span>
|
||||
<span>{ eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' }</span>
|
||||
</div>
|
||||
<div className="mx-2 text-4xl">·</div>
|
||||
</>
|
||||
)}
|
||||
<div>{ live ? <Counter startTime={startedAt} /> : formattedDuration }</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "30%", height: "38px" }} className="px-2 flex flex-col justify-between">
|
||||
<CountryFlag country={ userCountry } className="mr-2" label />
|
||||
<div className="color-gray-medium flex items-center">
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userBrowser) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
<div className="mx-2 text-4xl">·</div>
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userOs) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
<div className="mx-2 text-4xl">·</div>
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userDeviceType) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{ isSessions && (
|
||||
<div style={{ width: "10%"}} className="self-center px-2 flex items-center">
|
||||
<ErrorBars count={issueTypes.length} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
{ isAssist && showActive && (
|
||||
<Label success className={cn("bg-green color-white text-right mr-4", { 'opacity-0' : !active})}>
|
||||
<span className="color-white">ACTIVE</span>
|
||||
</Label>
|
||||
)}
|
||||
<div className={ stl.playLink } id="play-button" data-viewed={ viewed }>
|
||||
{ isSessions && (
|
||||
<div className="mr-4 flex-shrink-0 w-24">
|
||||
{ isLastPlayed && (
|
||||
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
|
||||
<span className="color-gray-medium text-xs" style={{ whiteSpace: 'nowrap'}}>LAST PLAYED</span>
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Link to={ isAssist ? liveSessionRoute(sessionId) : sessionRoute(sessionId) }>
|
||||
<Icon name={ !viewed && !isAssist ? 'play-fill' : 'play-circle-light' } size="42" color={isAssist ? "tealx" : "teal"} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ _metaList.length > 0 && (
|
||||
<SessionMetaList className="mt-4" metaList={_metaList} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
timezone: localStorage.getItem(TIMEZONE) || '',
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
}), { toggleFavorite, setSessionPath })(withRouter(SessionItem));
|
||||
|
|
@ -6,7 +6,7 @@ import MetaMoreButton from '../MetaMoreButton';
|
|||
|
||||
interface Props {
|
||||
className?: string,
|
||||
metaList: [],
|
||||
metaList: any[],
|
||||
maxLength?: number,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,7 @@
|
|||
|
||||
@import 'icons.css';
|
||||
@import 'mixins.css';
|
||||
|
||||
@keyframes fade {
|
||||
0% { opacity: 1}
|
||||
50% { opacity: 0}
|
||||
100% { opacity: 1}
|
||||
}
|
||||
|
||||
.sessionItem {
|
||||
user-select: none;
|
||||
@mixin defaultHover;
|
||||
border-radius: 3px;
|
||||
/* padding: 10px 10px; */
|
||||
/* padding-right: 15px; */
|
||||
/* margin-bottom: 15px; */
|
||||
/* background-color: white; */
|
||||
/* display: flex; */
|
||||
/* align-items: center; */
|
||||
border: solid thin #EEEEEE;
|
||||
|
||||
& .favorite {
|
||||
|
|
@ -39,7 +23,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
& .iconStack {
|
||||
/* & .iconStack {
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
& .icons {
|
||||
|
|
@ -48,7 +32,7 @@
|
|||
margin-bottom: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
& .left {
|
||||
& > div {
|
||||
|
|
@ -114,7 +98,4 @@
|
|||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
& svg {
|
||||
animation: fade 1s infinite;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Icon, Toggler, Button, Input } from 'UI';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Icon, Toggler, Button, Input, Loader } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
|
|
@ -7,11 +7,19 @@ function CaptureRate(props) {
|
|||
const [changed, setChanged] = React.useState(false);
|
||||
const { settingsStore } = useStore();
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
|
||||
const loading = useObserver(() => settingsStore.loadingCaptureRate)
|
||||
const [captureRate, setCaptureRate] = React.useState(sessionSettings.captureRate);
|
||||
const [captureAll, setCaptureAll] = React.useState(captureRate === 100);
|
||||
const [captureAll, setCaptureAll] = React.useState(sessionSettings.captureAll);
|
||||
|
||||
return (
|
||||
<>
|
||||
useEffect(() => {
|
||||
settingsStore.fetchCaptureRate().then(() => {
|
||||
setCaptureRate(sessionSettings.captureRate);
|
||||
setCaptureAll(sessionSettings.captureAll);
|
||||
});
|
||||
}, [])
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<h3 className="text-lg">Capture Rate</h3>
|
||||
<div className="my-1">What percentage of your user sessions do you want to record and monitor?</div>
|
||||
<div className="mt-2 mb-4">
|
||||
|
|
@ -42,10 +50,18 @@ function CaptureRate(props) {
|
|||
<Icon className="absolute right-0 mr-6 top-0 bottom-0 m-auto" name="percent" color="gray-medium" size="18" />
|
||||
</div>
|
||||
<span className="mx-3">of the sessions</span>
|
||||
<Button disabled={!changed} outline size="medium" onClick={settingsStore.updateCaptureRate(captureRate)}>Update</Button>
|
||||
<Button
|
||||
disabled={!changed}
|
||||
outline
|
||||
size="medium"
|
||||
onClick={() => settingsStore.saveCaptureRate({
|
||||
rate: captureRate,
|
||||
captureAll,
|
||||
}).finally(() => setChanged(false))}
|
||||
>Update</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
</Loader>
|
||||
));
|
||||
}
|
||||
|
||||
export default CaptureRate;
|
||||
|
|
@ -4,33 +4,38 @@ import Select from 'Shared/Select';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
const str = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)
|
||||
const d = str && str[1] || 'UTC';
|
||||
const timezoneOptions = [
|
||||
{ label: d, value: 'local' },
|
||||
{ label: 'UTC', value: 'UTC' },
|
||||
{ label: 'EST', value: 'EST' },
|
||||
]
|
||||
|
||||
function DefaultTimezone(props) {
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const { settingsStore } = useStore();
|
||||
const [timezone, setTimezone] = React.useState(settingsStore.sessionSettings.timezone);
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
|
||||
console.log('timezone', timezone)
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-lg">Default Timezone</h3>
|
||||
<div className="my-1">Session Time</div>
|
||||
<div className="mt-2 flex items-center" style={{ width: "200px"}}>
|
||||
<div className="mt-2 flex items-center" style={{ width: "220px"}}>
|
||||
<Select
|
||||
options={timezoneOptions}
|
||||
defaultValue={sessionSettings.timezone}
|
||||
className="w-4/6"
|
||||
onChange={(e, { value }) => {
|
||||
sessionSettings.updateKey('timezone', value);
|
||||
defaultValue={timezone}
|
||||
className="w-full"
|
||||
onChange={({ value }) => {
|
||||
setTimezone(value);
|
||||
setChanged(true);
|
||||
}}
|
||||
/>
|
||||
<div className="col-span-3 ml-3">
|
||||
<Button disabled={!changed} outline size="medium" onClick={() => {
|
||||
setChanged(false);
|
||||
sessionSettings.updateKey('timezone', timezone);
|
||||
}}>Update</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ const periodOptions = [
|
|||
]
|
||||
|
||||
function ListingVisibility(props) {
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const { settingsStore } = useStore();
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const [durationSettings, setDurationSettings] = React.useState(sessionSettings.durationFilter);
|
||||
|
||||
return (
|
||||
|
|
@ -24,28 +24,41 @@ function ListingVisibility(props) {
|
|||
<h3 className="text-lg">Listing Visibility</h3>
|
||||
<div className="my-1">Do not show sessions duration with.</div>
|
||||
<div className="grid grid-cols-12 gap-2 mt-2">
|
||||
<div className="col-span-3">
|
||||
<div className="col-span-4">
|
||||
<Select
|
||||
options={numberOptions}
|
||||
defaultValue={durationSettings.operator}
|
||||
onChange={({ value }) => {
|
||||
setDurationSettings({ ...durationSettings, operator: value });
|
||||
setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
value={durationSettings.count}
|
||||
type="number"
|
||||
name="count"
|
||||
style={{ height: '38px', width: '100%'}}
|
||||
onChange={(e, { value }) => {
|
||||
setDurationSettings({ ...durationSettings, count: value });
|
||||
setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
defaultValue={durationSettings.countType}
|
||||
options={periodOptions}
|
||||
onChange={({ value }) => {
|
||||
setDurationSettings({ ...durationSettings, countType: value });
|
||||
setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Button outline size="medium" disabled={!changed} onClick={() => {
|
||||
sessionSettings.updateKey('durationFilter', durationSettings);
|
||||
setChanged(false);
|
||||
}}>Update</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,3 +20,4 @@ export {
|
|||
WEBHOOK as CHANNEL_WEBHOOK
|
||||
} from './schedule';
|
||||
export { default } from './filterOptions';
|
||||
export { default as storageKeys } from './storageKeys';
|
||||
3
frontend/app/constants/storageKeys.ts
Normal file
3
frontend/app/constants/storageKeys.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const SKIP_TO_ISSUE = "__$session-skipToIssue$__"
|
||||
export const TIMEZONE = "__$session-timezone$__"
|
||||
export const DURATION_FILTER = "__$session-durationFilter$__"
|
||||
|
|
@ -9,6 +9,7 @@ import { fetchList as fetchSessionList } from './sessions';
|
|||
import { fetchList as fetchErrorsList } from './errors';
|
||||
import { FilterCategory, FilterKey, IssueType } from 'Types/filter/filterType';
|
||||
import { filtersMap, liveFiltersMap, generateFilterOptions, generateLiveFilterOptions } from 'Types/filter/newFilter';
|
||||
import { DURATION_FILTER } from 'App/constants/storageKeys'
|
||||
|
||||
const ERRORS_ROUTE = errorsRoute();
|
||||
|
||||
|
|
@ -149,6 +150,28 @@ export const reduceThenFetchResource = actionCreator => (...args) => (dispatch,
|
|||
filter.limit = 10;
|
||||
filter.page = getState().getIn([ 'search', 'currentPage']);
|
||||
|
||||
// duration filter from local storage
|
||||
if (!filter.filters.find(f => f.type === FilterKey.DURATION)) {
|
||||
const durationFilter = JSON.parse(localStorage.getItem(DURATION_FILTER) || '{"count": 0}');
|
||||
let durationValue = parseInt(durationFilter.count)
|
||||
if (durationValue > 0) {
|
||||
const value = [0];
|
||||
durationValue = durationFilter.countType === 'min' ? durationValue * 60 * 1000 : durationValue * 1000;
|
||||
if (durationFilter.operator === '<') {
|
||||
value[0] = durationValue;
|
||||
} else if (durationFilter.operator === '>') {
|
||||
value[1] = durationValue;
|
||||
}
|
||||
|
||||
filter.filters = filter.filters.concat({
|
||||
type: FilterKey.DURATION,
|
||||
operator: 'is',
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return isRoute(ERRORS_ROUTE, window.location.pathname)
|
||||
? dispatch(fetchErrorsList(filter))
|
||||
: dispatch(fetchSessionList(filter));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import DashboardStore, { IDashboardSotre } from './dashboardStore';
|
||||
import MetricStore, { IMetricStore } from './metricStore';
|
||||
import APIClient from 'App/api_client';
|
||||
import { dashboardService, metricService } from 'App/services';
|
||||
import { dashboardService, metricService, sessionService } from 'App/services';
|
||||
import SettingsStore from './settingsStore';
|
||||
|
||||
export class RootStore {
|
||||
|
|
@ -20,6 +20,7 @@ export class RootStore {
|
|||
const client = new APIClient();
|
||||
dashboardService.initClient(client)
|
||||
metricService.initClient(client)
|
||||
sessionService.initClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
|
||||
import { makeAutoObservable, observable, action } from "mobx"
|
||||
import SessionSettings from "./types/sessionSettings"
|
||||
import { sessionService } from "App/services"
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export default class SettingsStore {
|
||||
loadingCaptureRate: boolean = false;
|
||||
sessionSettings: SessionSettings = new SessionSettings()
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
|
|
@ -9,7 +12,29 @@ export default class SettingsStore {
|
|||
})
|
||||
}
|
||||
|
||||
updateCaptureRate(value: number) {
|
||||
this.sessionSettings.updateKey('captureRate', value);
|
||||
saveCaptureRate(data: any) {
|
||||
return sessionService.saveCaptureRate(data)
|
||||
.then(data => {
|
||||
this.sessionSettings.merge({
|
||||
captureRate: data.rate,
|
||||
captureAll: data.captureAll
|
||||
})
|
||||
toast.success("Capture rate saved successfully");
|
||||
}).catch(err => {
|
||||
toast.error("Error saving capture rate");
|
||||
})
|
||||
}
|
||||
|
||||
fetchCaptureRate(): Promise<any> {
|
||||
this.loadingCaptureRate = true;
|
||||
return sessionService.fetchCaptureRate()
|
||||
.then(data => {
|
||||
this.sessionSettings.merge({
|
||||
captureRate: data.rate,
|
||||
captureAll: data.captureAll
|
||||
})
|
||||
}).finally(() => {
|
||||
this.loadingCaptureRate = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
|
||||
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER } from 'App/constants/storageKeys'
|
||||
|
||||
export default class SessionSettings {
|
||||
skipToIssue: boolean = false
|
||||
timezone: string = "EST"
|
||||
durationFilter: any = {
|
||||
count: 0,
|
||||
countType: 'min',
|
||||
operator: '>'
|
||||
}
|
||||
skipToIssue: boolean = localStorage.getItem(SKIP_TO_ISSUE) === 'true';
|
||||
timezone: string = localStorage.getItem(TIMEZONE) || 'UTC';
|
||||
durationFilter: any = JSON.parse(localStorage.getItem(DURATION_FILTER) || '{}');
|
||||
captureRate: number = 0
|
||||
captureAll: boolean = false
|
||||
|
||||
|
|
@ -17,10 +14,25 @@ export default class SessionSettings {
|
|||
})
|
||||
}
|
||||
|
||||
merge(settings: any) {
|
||||
for (const key in settings) {
|
||||
if (settings.hasOwnProperty(key)) {
|
||||
this.updateKey(key, settings[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
console.log(`SessionSettings.updateKey(${key}, ${value})`)
|
||||
runInAction(() => {
|
||||
this[key] = value
|
||||
})
|
||||
|
||||
if (key === 'captureRate' || key === 'captureAll') return
|
||||
|
||||
if (key === 'durationFilter') {
|
||||
localStorage.setItem(`__$session-${key}$__`, JSON.stringify(value));
|
||||
} else {
|
||||
localStorage.setItem(`__$session-${key}$__`, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export default class Cursor {
|
|||
}
|
||||
|
||||
click() {
|
||||
console.log("clickong ", styles.clicked)
|
||||
this.cursor.classList.add(styles.clicked)
|
||||
setTimeout(() => {
|
||||
this.cursor.classList.remove(styles.clicked)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const HIGHEST_SPEED = 16;
|
|||
|
||||
const SPEED_STORAGE_KEY = "__$player-speed$__";
|
||||
const SKIP_STORAGE_KEY = "__$player-skip$__";
|
||||
const SKIP_TO_ISSUE_STORAGE_KEY = "__$player-skip-to-issue$__";
|
||||
const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__";
|
||||
const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__";
|
||||
const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__";
|
||||
const storedSpeed: number = parseInt(localStorage.getItem(SPEED_STORAGE_KEY) || "") ;
|
||||
|
|
|
|||
23
frontend/app/services/SessionService.ts
Normal file
23
frontend/app/services/SessionService.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import APIClient from 'App/api_client';
|
||||
|
||||
export default class SettingsService {
|
||||
private client: APIClient;
|
||||
|
||||
constructor(client?: APIClient) {
|
||||
this.client = client ? client : new APIClient();
|
||||
}
|
||||
|
||||
initClient(client?: APIClient) {
|
||||
this.client = client || new APIClient();
|
||||
}
|
||||
|
||||
saveCaptureRate(data: any) {
|
||||
return this.client.post('/sample_rate', data);
|
||||
}
|
||||
|
||||
fetchCaptureRate() {
|
||||
return this.client.get('/sample_rate')
|
||||
.then(response => response.json())
|
||||
.then(response => response.data || 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import DashboardService, { IDashboardService } from "./DashboardService";
|
||||
import MetricService, { IMetricService } from "./MetricService";
|
||||
import SessionSerivce from "./SessionService";
|
||||
|
||||
export const dashboardService: IDashboardService = new DashboardService();
|
||||
export const metricService: IMetricService = new MetricService();
|
||||
export const metricService: IMetricService = new MetricService();
|
||||
export const sessionService: SessionSerivce = new SessionSerivce();
|
||||
Loading…
Add table
Reference in a new issue