feat(ui) - session settings - capture rate api update

This commit is contained in:
Shekar Siri 2022-05-03 12:26:42 +02:00
parent 87f76f484d
commit 18e932e5e9
25 changed files with 366 additions and 74 deletions

View file

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

View file

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

View file

@ -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"),

View file

@ -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%; }
}

View file

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

View file

@ -4,7 +4,9 @@ import Modal from './Modal';
const ModalContext = createContext({
component: null,
props: {},
props: {
right: false,
},
showModal: (component: any, props: any) => {},
hideModal: () => {}
});

View file

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

View file

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

View file

@ -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' ]),

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

View file

@ -6,7 +6,7 @@ import MetaMoreButton from '../MetaMoreButton';
interface Props {
className?: string,
metaList: [],
metaList: any[],
maxLength?: number,
}

View file

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

View file

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

View file

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

View file

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

View file

@ -20,3 +20,4 @@ export {
WEBHOOK as CHANNEL_WEBHOOK
} from './schedule';
export { default } from './filterOptions';
export { default as storageKeys } from './storageKeys';

View file

@ -0,0 +1,3 @@
export const SKIP_TO_ISSUE = "__$session-skipToIssue$__"
export const TIMEZONE = "__$session-timezone$__"
export const DURATION_FILTER = "__$session-durationFilter$__"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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);
}
}

View file

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