feat(ui) - session settings - ui and state

This commit is contained in:
Shekar Siri 2022-05-02 16:07:00 +02:00
parent 122705b4c7
commit a1b656dc6a
18 changed files with 442 additions and 15 deletions

View file

@ -20,7 +20,6 @@ import { LAST_7_DAYS } from 'Types/app/period';
import { resetFunnel } from 'Duck/funnels';
import { resetFunnelFilters } from 'Duck/funnelFilters'
import NoSessionsMessage from 'Shared/NoSessionsMessage';
// import TrackerUpdateMessage from 'Shared/TrackerUpdateMessage';
import SessionSearch from 'Shared/SessionSearch';
import MainSearchBar from 'Shared/MainSearchBar';
import { clearSearch, fetchSessions } from 'Duck/search';
@ -130,7 +129,6 @@ export default class BugFinder extends React.PureComponent {
/>
</div>
<div className={cn("side-menu-margined", stl.searchWrapper) }>
{/* <TrackerUpdateMessage /> */}
<NoSessionsMessage />
<div className="mb-5">
<MainSearchBar />

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import { connect } from 'react-redux';
import cn from 'classnames';
import { SideMenuitem, SavedSearchList, Progress, Popup } from 'UI'
@ -7,17 +7,23 @@ import { fetchWatchdogStatus } from 'Duck/watchdogs';
import { clearEvents } from 'Duck/filters';
import { issues_types } from 'Types/session/issue'
import { fetchList as fetchSessionList } from 'Duck/sessions';
import { useModal } from 'App/components/Modal';
import SessionSettings from 'Shared/SessionSettings/SessionSettings'
function SessionsMenu(props) {
const { activeTab, keyMap, wdTypeCount, toggleRehydratePanel } = props;
const { hideModal, showModal } = useModal();
const onMenuItemClick = (filter) => {
props.onMenuItemClick(filter)
}
const capturingAll = props.captureRate && props.captureRate.get('captureAll');
useEffect(() => {
showModal(<SessionSettings />, {});
}, [])
return (
<div className={stl.wrapper}>
<div className={ cn(stl.header, 'flex items-center') }>

View file

@ -0,0 +1,63 @@
import React from 'react';
import Select from 'react-select';
interface Props {
options: any[];
isSearchable?: boolean;
defaultValue?: any;
plain?: boolean;
[x:string]: any;
}
export default function({ plain = false, options, isSearchable = false, defaultValue, ...rest }: Props) {
const customStyles = {
option: (provided, state) => ({
...provided,
whiteSpace: 'nowrap',
}),
menu: (provided, state) => ({
...provided,
top: 31,
}),
control: (provided) => {
const obj = {
...provided,
border: 'solid thin #ddd'
}
if (plain) {
obj['border'] = '1px solid transparent'
}
return obj;
},
valueContainer: (provided) => ({
...provided,
paddingRight: '0px',
}),
singleValue: (provided, state) => {
const opacity = state.isDisabled ? 0.5 : 1;
const transition = 'opacity 300ms';
return { ...provided, opacity, transition };
}
}
return (
<Select
options={options}
isSearchable={isSearchable}
defaultValue={defaultValue ? options.find(i => i.value === defaultValue) : options[0]}
components={{
IndicatorSeparator: () => null
}}
styles={customStyles}
theme={(theme) => ({
...theme,
colors: {
...theme.colors,
primary: '#394EFF',
}
})}
{...rest}
/>
);
}
// export default Select;

View file

@ -0,0 +1 @@
export { default } from './Select';

View file

@ -0,0 +1,40 @@
import React, { useEffect } from 'react';
import { Input, Button, Toggler, Icon } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import ListingVisibility from './components/ListingVisibility';
import DefaultPlaying from './components/DefaultPlaying';
import DefaultTimezone from './components/DefaultTimezone';
import CaptureRate from './components/CaptureRate';
function SessionSettings(props) {
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
return useObserver(() => (
<div className="bg-white box-shadow h-screen" style={{ width: '450px'}}>
<div className="p-6">
<h1 className="text-2xl">Session Settings</h1>
</div>
<div className="p-6 border-b py-8">
<ListingVisibility />
</div>
<div className="p-6 border-b py-8">
<DefaultPlaying />
</div>
<div className="p-6 border-b py-8">
<DefaultTimezone />
</div>
<div className="p-6 py-8">
<CaptureRate />
</div>
</div>
));
}
export default SessionSettings;

View file

@ -0,0 +1,43 @@
import React from 'react';
import { Icon, Toggler, Button, Input } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
function CaptureRate(props) {
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
const [captureRate, setCaptureRate] = React.useState(sessionSettings.captureRate);
const [captureAll, setCaptureAll] = React.useState(captureRate === 100);
return (
<>
<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">
<Toggler
checked={captureAll}
name="test"
onChange={() => setCaptureAll(!captureAll)}
label="Capture 100% of the sessions"
/>
</div>
<div className="flex items-center">
<div className="relative">
<Input
type="number"
value={captureRate}
style={{ height: '38px', width: '100px'}}
onChange={(e, { value }) => setCaptureRate(value)}
disabled={captureAll}
min={0}
minValue={0}
/>
<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 outline size="medium" onClick={settingsStore.updateCaptureRate(captureRate)}>Update</Button>
</div>
</>
);
}
export default CaptureRate;

View file

@ -0,0 +1,25 @@
import React from 'react';
import { Toggler } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
function DefaultPlaying(props) {
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
return useObserver(() => (
<>
<h3 className="text-lg">Default Playing Options</h3>
<div className="my-1">Always start playing the session from the first issue.</div>
<div className="mt-2">
<Toggler
checked={sessionSettings.skipToIssue}
name="test"
onChange={() => sessionSettings.updateKey('skipToIssue', !sessionSettings.skipToIssue)}
/>
</div>
</>
));
}
export default DefaultPlaying;

View file

@ -0,0 +1,42 @@
import React, { useEffect } from 'react';
import { Toggler, Button } from 'UI';
import Select from 'Shared/Select';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
const timezoneOptions = [
{ label: 'UTC', value: 'UTC' },
{ label: 'EST', value: 'EST' },
]
function DefaultTimezone(props) {
const [changed, setChanged] = React.useState(false);
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
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"}}>
<Select
options={timezoneOptions}
defaultValue={sessionSettings.timezone}
className="w-4/6"
onChange={(e, { value }) => {
sessionSettings.updateKey('timezone', value);
setChanged(true);
}}
/>
<div className="col-span-3 ml-3">
<Button disabled={!changed} outline size="medium" onClick={() => {
setChanged(false);
}}>Update</Button>
</div>
</div>
<div className="text-sm mt-3">This change will impact the timestamp on session card and player.</div>
</>
);
}
export default DefaultTimezone;

View file

@ -0,0 +1,57 @@
import React from 'react';
import Select from 'Shared/Select';
import { Button, Input } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
const numberOptions = [
{ label: 'Less than', value: '<' },
{ label: 'Greater than', value: '>' },
]
const periodOptions = [
{ label: 'Mins', value: 'min' },
{ label: 'Secs', value: 'sec' },
]
function ListingVisibility(props) {
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
const [changed, setChanged] = React.useState(false);
const [durationSettings, setDurationSettings] = React.useState(sessionSettings.durationFilter);
return (
<>
<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">
<Select
options={numberOptions}
defaultValue={durationSettings.operator}
/>
</div>
<div className="col-span-3">
<Input
value={durationSettings.count}
type="number"
name="count"
style={{ height: '38px', width: '100%'}}
/>
</div>
<div className="col-span-3">
<Select
defaultValue={durationSettings.countType}
options={periodOptions}
/>
</div>
<div className="col-span-3">
<Button outline size="medium" disabled={!changed} onClick={() => {
setChanged(false);
}}>Update</Button>
</div>
</div>
</>
);
}
export default ListingVisibility;

View file

@ -0,0 +1 @@
export { default } from './SessionSettings';

View file

@ -0,0 +1,84 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const CheckedIcon = () => <>🌜</>;
const UncheckedIcon = () => <>🌞</>;
const ToggleButton = ( props ) => {
const [toggle, setToggle] = useState(false);
const { defaultChecked, onChange, disabled, className } = props;
useEffect(() => {
if (defaultChecked) {
setToggle(defaultChecked)
}
}, [defaultChecked]);
const triggerToggle = () => {
if ( disabled ) {
return;
}
setToggle(!toggle);
if ( typeof onChange === 'function' ) {
onChange(!toggle);
}
}
const getIcon = (type) => {
const { icons } = props;
if ( ! icons ) {
return null;
}
return icons[type] === undefined ?
ToggleButton.defaultProps.icons[type] :
icons[type];
}
const toggleClasses = classNames('wrg-toggle', {
'wrg-toggle--checked': toggle,
'wrg-toggle--disabled': disabled
}, className);
return (
<div onClick={triggerToggle} className={toggleClasses}>
<div className="wrg-toggle-container">
<div className="wrg-toggle-check">
<span>{ getIcon('checked') }</span>
</div>
<div className="wrg-toggle-uncheck">
<span>{ getIcon('unchecked') }</span>
</div>
</div>
<div className="wrg-toggle-circle"></div>
<input type="checkbox" aria-label="Toggle Button" className="wrg-toggle-input" />
</div>
);
}
ToggleButton.defaultProps = {
icons: {
checked: <CheckedIcon />,
unchecked: <UncheckedIcon />
}
};
ToggleButton.propTypes = {
disabled: PropTypes.bool,
defaultChecked: PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func,
icons: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.shape({
checked: PropTypes.node,
unchecked: PropTypes.node
})
])
};
export default ToggleButton;

View file

@ -3,18 +3,22 @@ import styles from './toggler.css';
export default ({
onChange,
name,
className,
className = '',
checked,
label = '',
}) => (
<div className={ className }>
<label className={ styles.switch }>
<input
type={ styles.checkbox }
onClick={ onChange }
name={ name }
checked={ checked }
/>
<span className={ `${ styles.slider } ${ checked ? styles.checked : '' }` } />
<label className={styles.label}>
<div className={ styles.switch }>
<input
type={ styles.checkbox }
onClick={ onChange }
name={ name }
checked={ checked }
/>
<span className={ `${ styles.slider } ${ checked ? styles.checked : '' }` } />
</div>
{ label && <span>{ label }</span> }
</label>
</div>
);

View file

@ -6,6 +6,16 @@
height: 16px;
}
.label {
display: flex;
align-items: center;
cursor: pointer;
& span {
padding-left: 10px;
color: $gray-medium;
}
}
.switch input {
display:none;
}
@ -29,18 +39,23 @@
width: 20px;
left: 0;
bottom: -2px;
background-color: white;
/* background-color: white; */
transition: .4s;
border-radius: 50%;
border: solid 1px rgba(0, 0, 0, 0.2);
background: #394EFF;
box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px rgba(0, 0, 0, 0.14), 0px 1px 3px rgba(0, 0, 0, 0.12);
}
.slider.checked {
background-color: $teal !important;
/* background-color: $teal !important; */
background-color: #b2bcff !important;
}
.slider.checked:before {
border: solid 1px $teal;
transform: translateX(15px);
}
.slider.checked:before {

View file

@ -58,5 +58,6 @@ export { default as HelpText } from './HelpText';
export { default as SideMenuHeader } from './SideMenuHeader';
export { default as PageTitle } from './PageTitle';
export { default as Pagination } from './Pagination';
export { default as Toggler } from './Toggler';
export { Input, Modal, Form, Message, Card } from 'semantic-ui-react';

View file

@ -3,14 +3,17 @@ import DashboardStore, { IDashboardSotre } from './dashboardStore';
import MetricStore, { IMetricStore } from './metricStore';
import APIClient from 'App/api_client';
import { dashboardService, metricService } from 'App/services';
import SettingsStore from './settingsStore';
export class RootStore {
dashboardStore: IDashboardSotre;
metricStore: IMetricStore;
settingsStore: SettingsStore;
constructor() {
this.dashboardStore = new DashboardStore();
this.metricStore = new MetricStore();
this.settingsStore = new SettingsStore();
}
initClient() {

View file

@ -0,0 +1,15 @@
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
import SessionSettings from "./types/sessionSettings"
export default class SettingsStore {
sessionSettings: SessionSettings = new SessionSettings()
constructor() {
makeAutoObservable(this, {
sessionSettings: observable,
})
}
updateCaptureRate(value: number) {
this.sessionSettings.updateKey('captureRate', value);
}
}

View file

@ -0,0 +1,26 @@
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
export default class SessionSettings {
skipToIssue: boolean = false
timezone: string = "EST"
durationFilter: any = {
count: 0,
countType: 'min',
operator: '>'
}
captureRate: number = 0
captureAll: boolean = false
constructor() {
makeAutoObservable(this, {
updateKey: action
})
}
updateKey(key: string, value: any) {
console.log(`SessionSettings.updateKey(${key}, ${value})`)
runInAction(() => {
this[key] = value
})
}
}

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-percent" viewBox="0 0 16 16">
<path d="M13.442 2.558a.625.625 0 0 1 0 .884l-10 10a.625.625 0 1 1-.884-.884l10-10a.625.625 0 0 1 .884 0zM4.5 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm7 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B