From 7005c046b808458514d2b9ec2169dfd3b17def40 Mon Sep 17 00:00:00 2001 From: sylenien Date: Thu, 12 May 2022 14:53:20 +0200 Subject: [PATCH 01/11] fix ui bugs in session tab --- .../shared/SessionItem/PlayLink/PlayLink.tsx | 42 +++++++++ .../shared/SessionItem/PlayLink/index.ts | 1 + .../shared/SessionItem/SessionItem.js | 11 ++- .../shared/SessionItem/sessionItem.css | 3 + .../ui/TextEllipsis/TextEllipsis.js | 91 ++++++++++++++++--- .../ui/TimezoneDropdown/TimezoneDropdown.js | 7 +- frontend/app/svg/icons/play-hover.svg | 12 +++ 7 files changed, 148 insertions(+), 19 deletions(-) create mode 100644 frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx create mode 100644 frontend/app/components/shared/SessionItem/PlayLink/index.ts create mode 100644 frontend/app/svg/icons/play-hover.svg diff --git a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx new file mode 100644 index 000000000..203b72089 --- /dev/null +++ b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx @@ -0,0 +1,42 @@ +import React, { useState, useEffect } from 'react' +import { + Link, + Icon, + } from 'UI'; +import { session as sessionRoute, liveSession as liveSessionRoute } from 'App/routes'; + +const PLAY_ICON_NAMES = { + notPlayed: 'play-fill', + played: 'play-circle-light', + hovered: 'play-hover' +} + +const getDefaultIconName = (isViewed) => !isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played + +interface Props { + isAssist: boolean; + viewed: boolean; + sessionId: string; +} +export default function PlayLink(props: Props) { + const { isAssist, viewed, sessionId } = props + const defaultIconName = getDefaultIconName(viewed) + + const [isHovered, toggleHover] = useState(false) + const [iconName, setIconName] = useState(defaultIconName) + + useEffect(() => { + if (isHovered) setIconName(PLAY_ICON_NAMES.hovered) + else setIconName(getDefaultIconName(viewed)) + }, [isHovered, viewed]) + + return ( + toggleHover(true)} + onMouseLeave={() => toggleHover(false)} + > + + + ) +} \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/PlayLink/index.ts b/frontend/app/components/shared/SessionItem/PlayLink/index.ts new file mode 100644 index 000000000..7ff31c16c --- /dev/null +++ b/frontend/app/components/shared/SessionItem/PlayLink/index.ts @@ -0,0 +1 @@ +export { default } from './PlayLink' \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js index af00fa2a2..2c24243ae 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ b/frontend/app/components/shared/SessionItem/SessionItem.js @@ -15,6 +15,7 @@ import stl from './sessionItem.css'; import Counter from './Counter' import { withRouter } from 'react-router-dom'; import SessionMetaList from './SessionMetaList'; +import PlayLink from './PlayLink'; import ErrorBars from './ErrorBars'; import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from "App/routes"; import { capitalize } from 'App/utils'; @@ -76,7 +77,7 @@ export default class SessionItem extends React.PureComponent { }); return ( -
+
@@ -144,9 +145,11 @@ export default class SessionItem extends React.PureComponent { )}
)} - - - +
diff --git a/frontend/app/components/shared/SessionItem/sessionItem.css b/frontend/app/components/shared/SessionItem/sessionItem.css index 4b042b160..9f7e03d37 100644 --- a/frontend/app/components/shared/SessionItem/sessionItem.css +++ b/frontend/app/components/shared/SessionItem/sessionItem.css @@ -1,5 +1,8 @@ +@import 'mixins.css'; .sessionItem { + background-color: #fff; + @mixin defaultHover; user-select: none; border-radius: 3px; border: solid thin #EEEEEE; diff --git a/frontend/app/components/ui/TextEllipsis/TextEllipsis.js b/frontend/app/components/ui/TextEllipsis/TextEllipsis.js index a6d3e6abc..0161eef6f 100644 --- a/frontend/app/components/ui/TextEllipsis/TextEllipsis.js +++ b/frontend/app/components/ui/TextEllipsis/TextEllipsis.js @@ -1,7 +1,42 @@ +import { useState, useRef, useEffect, forwardRef } from 'react'; import cn from 'classnames'; import { Popup } from 'UI'; import styles from './textEllipsis.css'; +/** calculates text width in pixels ++ * by creating a hidden element with t ++ * ext and counting its width ++ * @param text String - text string ++ * @param fontProp String - font properties ++ * @returns width number ++ */ +function findTextWidth(text, fontProp) { + const tag = document.createElement('div') + + tag.style.position = 'absolute' + tag.style.left = '-99in' + tag.style.whiteSpace = 'nowrap' + tag.style.font = fontProp + tag.innerHTML = text + + document.body.appendChild(tag) + const result = tag.clientWidth + document.body.removeChild(tag) + + return result; +} + +const Trigger = forwardRef(({ textOrChildren, maxWidth, style, className, ...rest }, ref) => ( +
+ { textOrChildren } +
+)) + const TextEllipsis = ({ text, hintText = text, @@ -14,23 +49,51 @@ const TextEllipsis = ({ hintProps={}, ...props }) => { + const [showPopup, setShowPopup] = useState(false) + const textRef = useRef(null); + const textOrChildren = text || children; - const trigger = ( -
- { textOrChildren } -
- ); - if (noHint) return trigger; + + const popupId = (Math.random() + 1).toString(36).substring(7); + + useEffect(() => { + const element = textRef.current; + + const fontSize = window.getComputedStyle(element, null).getPropertyValue('font-size'); + + const textWidth = findTextWidth(element.innerText, fontSize) + console.log(textWidth, element.clientWidth) + if (textWidth > element.clientWidth) setShowPopup(true) + else setShowPopup(false) + }, [textRef.current]) + + if (noHint || !showPopup) return ( + + ) + return ( { hintText || textOrChildren }
} - { ...popupProps } - /> + trigger={ + + } + content={
{ hintText || textOrChildren }
} + { ...popupProps } + /> ); }; diff --git a/frontend/app/components/ui/TimezoneDropdown/TimezoneDropdown.js b/frontend/app/components/ui/TimezoneDropdown/TimezoneDropdown.js index bafebfa76..2b7b8834e 100644 --- a/frontend/app/components/ui/TimezoneDropdown/TimezoneDropdown.js +++ b/frontend/app/components/ui/TimezoneDropdown/TimezoneDropdown.js @@ -5,8 +5,13 @@ import stl from './timezoneDropdown.css'; import { connect } from 'react-redux'; import { setTimezone } from 'Duck/sessions'; +const localMachineFormat = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1] +const middlePoint = localMachineFormat.length - 2 +const readableLocalTimezone = + `${localMachineFormat.substring(0, 3)} ${localMachineFormat.substring(3, middlePoint)}:${localMachineFormat.substring(middlePoint)}` + const timezoneOptions = { - 'local': new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1], + 'local': readableLocalTimezone, 'UTC': 'UTC' }; diff --git a/frontend/app/svg/icons/play-hover.svg b/frontend/app/svg/icons/play-hover.svg new file mode 100644 index 000000000..4939d51d9 --- /dev/null +++ b/frontend/app/svg/icons/play-hover.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + From 1f0fb80024dbe051db53cd0c28d134175d8cc8a8 Mon Sep 17 00:00:00 2001 From: sylenien Date: Thu, 12 May 2022 15:26:19 +0200 Subject: [PATCH 02/11] fix category and filters naming, add underline to username hover, fix small bugs --- frontend/app/components/shared/SessionItem/SessionItem.js | 2 +- .../app/components/shared/SessionItem/sessionItem.css | 5 +++++ frontend/app/components/ui/IconButton/IconButton.js | 2 +- frontend/app/components/ui/TextEllipsis/TextEllipsis.js | 1 - frontend/app/svg/icons/filters/arrow-return-right.svg | 3 +++ frontend/app/types/filter/filter.js | 3 +-- frontend/app/types/filter/filterType.ts | 4 ++-- frontend/app/types/filter/newFilter.js | 8 ++++---- 8 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 frontend/app/svg/icons/filters/arrow-return-right.svg diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js index 2c24243ae..6222327f3 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ b/frontend/app/components/shared/SessionItem/SessionItem.js @@ -84,7 +84,7 @@ export default class SessionItem extends React.PureComponent {
(!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)} > diff --git a/frontend/app/components/shared/SessionItem/sessionItem.css b/frontend/app/components/shared/SessionItem/sessionItem.css index 9f7e03d37..cb7b87c3a 100644 --- a/frontend/app/components/shared/SessionItem/sessionItem.css +++ b/frontend/app/components/shared/SessionItem/sessionItem.css @@ -101,4 +101,9 @@ text-transform: uppercase; font-size: 10px; letter-spacing: 1px; +} + +.userName:hover { + text-decoration: underline; + text-decoration-color: $teal; } \ No newline at end of file diff --git a/frontend/app/components/ui/IconButton/IconButton.js b/frontend/app/components/ui/IconButton/IconButton.js index 6aa9f3d5f..0b62c41de 100644 --- a/frontend/app/components/ui/IconButton/IconButton.js +++ b/frontend/app/components/ui/IconButton/IconButton.js @@ -26,7 +26,7 @@ const IconButton = React.forwardRef(({ name, disabled = false, tooltip = false, - tooltipPosition = 'top', + tooltipPosition = 'top center', compact = false, ...rest }, ref) => ( diff --git a/frontend/app/components/ui/TextEllipsis/TextEllipsis.js b/frontend/app/components/ui/TextEllipsis/TextEllipsis.js index 0161eef6f..b3528ff23 100644 --- a/frontend/app/components/ui/TextEllipsis/TextEllipsis.js +++ b/frontend/app/components/ui/TextEllipsis/TextEllipsis.js @@ -62,7 +62,6 @@ const TextEllipsis = ({ const fontSize = window.getComputedStyle(element, null).getPropertyValue('font-size'); const textWidth = findTextWidth(element.innerText, fontSize) - console.log(textWidth, element.clientWidth) if (textWidth > element.clientWidth) setShowPopup(true) else setShowPopup(false) }, [textRef.current]) diff --git a/frontend/app/svg/icons/filters/arrow-return-right.svg b/frontend/app/svg/icons/filters/arrow-return-right.svg new file mode 100644 index 000000000..228e24b4a --- /dev/null +++ b/frontend/app/svg/icons/filters/arrow-return-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index 9566c9d4b..06cab8d14 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -121,9 +121,8 @@ export const defaultFilters = [ { label: 'Time Between Events', key: KEYS.TIME_BETWEEN_EVENTS, type: KEYS.TIME_BETWEEN_EVENTS, filterKey: KEYS.TIME_BETWEEN_EVENTS, icon: 'filters/click', isFilter: false }, { label: 'Avg CPU Load', key: KEYS.AVG_CPU_LOAD, type: KEYS.AVG_CPU_LOAD, filterKey: KEYS.AVG_CPU_LOAD, icon: 'filters/click', isFilter: false }, { label: 'Memory Usage', key: KEYS.AVG_MEMORY_USAGE, type: KEYS.AVG_MEMORY_USAGE, filterKey: KEYS.AVG_MEMORY_USAGE, icon: 'filters/click', isFilter: false }, - { label: 'Input', key: KEYS.INPUT, type: KEYS.INPUT, filterKey: KEYS.INPUT, icon: 'event/input', isFilter: false }, - { label: 'Page', key: KEYS.LOCATION, type: KEYS.LOCATION, filterKey: KEYS.LOCATION, icon: 'event/link', isFilter: false }, + { label: 'Path', key: KEYS.LOCATION, type: KEYS.LOCATION, filterKey: KEYS.LOCATION, icon: 'event/link', isFilter: false }, // { label: 'View', key: KEYS.VIEW, type: KEYS.VIEW, filterKey: KEYS.VIEW, icon: 'event/view', isFilter: false } ] }, diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index 83511e20a..f77d15a11 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -3,8 +3,8 @@ export enum FilterCategory { GEAR = "Gear", RECORDING_ATTRIBUTES = "Recording Attributes", JAVASCRIPT = "Javascript", - USER = "User", - METADATA = "Metadata", + USER = "User Identification", + METADATA = "Session & User Metadata", PERFORMANCE = "Performance", }; diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 22a27f144..725b94560 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -11,7 +11,7 @@ export const filtersMap = { // EVENTS [FilterKey.CLICK]: { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true }, [FilterKey.INPUT]: { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true }, - [FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Page', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true }, + [FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true }, [FilterKey.CUSTOM]: { key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true }, // [FilterKey.REQUEST]: { key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true }, [FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, operator: 'is', label: 'Network Request', filters: [ @@ -37,10 +37,10 @@ export const filtersMap = { [FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' }, [FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' }, [FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions }, - [FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'RevId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/rev-id' }, - [FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/referrer' }, + [FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'Version ID', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'collection' }, + [FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' }, [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is']), icon: 'filters/duration' }, - [FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions }, + [FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions }, // [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' }, [FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ text: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' }, [FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' }, From 1529510d25fa25b47738828201bf5d62cd34656d Mon Sep 17 00:00:00 2001 From: sylenien Date: Thu, 12 May 2022 16:31:46 +0200 Subject: [PATCH 03/11] removed browser autocomplete from filter inputs, removed timezone picker from main page --- .../BugFinder/AutoComplete/AutoComplete.js | 28 +++++++++---------- .../SessionList/SessionListHeader.js | 6 +--- .../EventFilter/AutoComplete/AutoComplete.js | 28 +++++++++---------- .../FilterAutoComplete/FilterAutoComplete.tsx | 15 +++++----- 4 files changed, 37 insertions(+), 40 deletions(-) diff --git a/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js b/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js index b528e7bb9..dbd6f9729 100644 --- a/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js +++ b/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js @@ -14,7 +14,7 @@ const SOME_ERROR_MSG = "Some error occured."; const defaultValueToText = value => value; const defaultOptionMapping = (values, valueToText) => values.map(value => ({ text: valueToText(value), value })); -const hiddenStyle = { +const hiddenStyle = { whiteSpace: 'pre-wrap', opacity: 0, position: 'fixed', left: '-3000px' }; @@ -32,10 +32,10 @@ class AutoComplete extends React.PureComponent { values: [], noResultsMessage: TYPE_TO_SEARCH_MSG, ddOpen: false, - query: this.props.value, + query: this.props.value, loading: false, error: false - } + } componentWillReceiveProps(newProps) { if (this.props.value !== newProps.value) { @@ -49,8 +49,8 @@ class AutoComplete extends React.PureComponent { requestValues = (q) => { const { params, endpoint, method } = this.props; - this.setState({ - loading: true, + this.setState({ + loading: true, error: false, }); return new APIClient()[ method.toLowerCase() ](endpoint, { ...params, q }) @@ -72,7 +72,7 @@ class AutoComplete extends React.PureComponent { debouncedRequestValues = debounce(this.requestValues, 1000) - setError = () => this.setState({ + setError = () => this.setState({ loading: false, error: true, noResultsMessage: SOME_ERROR_MSG, @@ -98,7 +98,7 @@ class AutoComplete extends React.PureComponent { const _value = value.trim(); onSelect(null, {name, value: _value}); } - + changed = false; pasted = false; } @@ -126,10 +126,10 @@ class AutoComplete extends React.PureComponent { } = this.props; const options = optionMapping(values, valueToText) - + return ( - {/* */} @@ -140,7 +140,6 @@ class AutoComplete extends React.PureComponent { onFocus={ () => this.setState({ddOpen: true})} onChange={ this.onInputChange } onBlur={ this.onBlur } - onFocus={ () => this.setState({ddOpen: true})} value={ query } autoFocus={ true } type="text" @@ -150,6 +149,7 @@ class AutoComplete extends React.PureComponent { this.hiddenInput.value = text; pasted = true; // to use only the hidden input } } + autocomplete="do-not-autofill-bad-chrome" />
{ showCloseButton ? : or} @@ -182,11 +182,11 @@ class AutoComplete extends React.PureComponent { { headerText && headerText } { options.map(item => ( - this.onItemClick(e, item) } - /> + onClick={ (e) => this.onItemClick(e, item) } + /> )) }
diff --git a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js index e4f949473..57d1a21b4 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js +++ b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js @@ -13,7 +13,7 @@ const sortOptionsMap = { 'startTs-desc': 'Newest', 'startTs-asc': 'Oldest', 'eventsCount-asc': 'Events Ascending', - 'eventsCount-desc': 'Events Descending', + 'eventsCount-desc': 'Events Descending', }; const sortOptions = Object.entries(sortOptionsMap) .map(([ value, text ]) => ({ value, text })); @@ -50,10 +50,6 @@ function SessionListHeader({ value='list' />
*/} -
- Timezone - -
Sort By diff --git a/frontend/app/components/shared/EventFilter/AutoComplete/AutoComplete.js b/frontend/app/components/shared/EventFilter/AutoComplete/AutoComplete.js index e075b17bd..c4588354b 100644 --- a/frontend/app/components/shared/EventFilter/AutoComplete/AutoComplete.js +++ b/frontend/app/components/shared/EventFilter/AutoComplete/AutoComplete.js @@ -13,7 +13,7 @@ const SOME_ERROR_MSG = "Some error occured."; const defaultValueToText = value => value; const defaultOptionMapping = (values, valueToText) => values.map(value => ({ text: valueToText(value), value })); -const hiddenStyle = { +const hiddenStyle = { whiteSpace: 'pre-wrap', opacity: 0, position: 'fixed', left: '-3000px' }; @@ -31,10 +31,10 @@ class AutoComplete extends React.PureComponent { values: [], noResultsMessage: TYPE_TO_SEARCH_MSG, ddOpen: false, - query: this.props.value, + query: this.props.value, loading: false, error: false - } + } componentWillReceiveProps(newProps) { if (this.props.value !== newProps.value) { @@ -48,8 +48,8 @@ class AutoComplete extends React.PureComponent { requestValues = (q) => { const { params, endpoint, method } = this.props; - this.setState({ - loading: true, + this.setState({ + loading: true, error: false, }); return new APIClient()[ method.toLowerCase() ](endpoint, { ...params, q }) @@ -71,13 +71,13 @@ class AutoComplete extends React.PureComponent { debouncedRequestValues = debounce(this.requestValues, 1000) - setError = () => this.setState({ + setError = () => this.setState({ loading: false, error: true, noResultsMessage: SOME_ERROR_MSG, }) - onInputChange = ({ target: { value } }) => { + onInputChange = ({ target: { value } }) => { changed = true; this.setState({ query: value, updated: true }) const _value = value.trim(); @@ -96,7 +96,7 @@ class AutoComplete extends React.PureComponent { const _value = value.trim(); onSelect(null, {name, value: _value}); } - + changed = false; pasted = false; } @@ -123,10 +123,10 @@ class AutoComplete extends React.PureComponent { } = this.props; const options = optionMapping(values, valueToText) - + return ( - {/* this.setState({ddOpen: true})} onChange={ this.onInputChange } onBlur={ this.onBlur } - onFocus={ () => this.setState({ddOpen: true})} value={ query } autoFocus={ true } type="text" @@ -163,6 +162,7 @@ class AutoComplete extends React.PureComponent { this.hiddenInput.value = text; pasted = true; // to use only the hidden input } } + autocomplete="do-not-autofill-bad-chrome" />
{/* */} @@ -175,10 +175,10 @@ class AutoComplete extends React.PureComponent { { headerText && headerText } { options.map(item => ( - this.onItemClick(e, item) } + onClick={ (e) => this.onItemClick(e, item) } /> // this.onItemClick(e, item) } /> )) diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index a97f3573c..fb0eb4a6b 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -5,7 +5,7 @@ import { debounce } from 'App/utils'; import stl from './FilterAutoComplete.css'; import cn from 'classnames'; -const hiddenStyle = { +const hiddenStyle = { whiteSpace: 'pre-wrap', opacity: 0, position: 'fixed', left: '-3000px' }; @@ -43,8 +43,8 @@ function FilterAutoComplete(props: Props) { const [loading, setLoading] = useState(false) const [options, setOptions] = useState([]); const [query, setQuery] = useState(value); - - const requestValues = (q) => { + + const requestValues = (q) => { setLoading(true); return new APIClient()[method?.toLocaleLowerCase()](endpoint, { ...params, q }) @@ -90,7 +90,7 @@ function FilterAutoComplete(props: Props) { e.preventDefault(); if (query !== item.value) { - setQuery(item.value); + setQuery(item.value); } props.onSelect(e, item); @@ -107,6 +107,7 @@ function FilterAutoComplete(props: Props) { autoFocus={ true } type="text" placeholder={ placeholder } + autoComplete="do-not-autofill-bad-chrome" // onPaste={(e) => { // const text = e.clipboardData.getData('Text'); // // this.hiddenInput.value = text; @@ -139,11 +140,11 @@ function FilterAutoComplete(props: Props) { > { icon && } { item.value } -
+
)) }
- )} + )} )} @@ -151,4 +152,4 @@ function FilterAutoComplete(props: Props) { ); } -export default FilterAutoComplete; \ No newline at end of file +export default FilterAutoComplete; From 64ebd07e577aab243c3f72d8b7567d375469c878 Mon Sep 17 00:00:00 2001 From: sylenien Date: Thu, 12 May 2022 16:33:28 +0200 Subject: [PATCH 04/11] added toggler disabled colors, visibility default values, no items warning text to search field --- .../shared/EventFilter/FilterModal/FilterModal.js | 2 +- .../SessionSettings/components/ListingVisibility.tsx | 9 +++++---- frontend/app/components/ui/Toggler/toggler.css | 3 +-- frontend/app/svg/icons/binoculars.svg | 3 +++ 4 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 frontend/app/svg/icons/binoculars.svg diff --git a/frontend/app/components/shared/EventFilter/FilterModal/FilterModal.js b/frontend/app/components/shared/EventFilter/FilterModal/FilterModal.js index cbe8c9546..2b8219894 100644 --- a/frontend/app/components/shared/EventFilter/FilterModal/FilterModal.js +++ b/frontend/app/components/shared/EventFilter/FilterModal/FilterModal.js @@ -119,7 +119,7 @@ export default class FilterModal extends React.PureComponent { return (!displayed ? null : -
+
{ filteredList.map(category => ( diff --git a/frontend/app/components/shared/SessionSettings/components/ListingVisibility.tsx b/frontend/app/components/shared/SessionSettings/components/ListingVisibility.tsx index d9f137daa..c358f9389 100644 --- a/frontend/app/components/shared/SessionSettings/components/ListingVisibility.tsx +++ b/frontend/app/components/shared/SessionSettings/components/ListingVisibility.tsx @@ -18,7 +18,7 @@ function ListingVisibility(props) { const { settingsStore } = useStore(); const sessionSettings = useObserver(() => settingsStore.sessionSettings) const [durationSettings, setDurationSettings] = React.useState(sessionSettings.durationFilter); - + return ( <>

Listing Visibility

@@ -27,7 +27,7 @@ function ListingVisibility(props) {
{ setDurationSettings({ ...durationSettings, countType: value }); @@ -67,4 +68,4 @@ function ListingVisibility(props) { ); } -export default ListingVisibility; \ No newline at end of file +export default ListingVisibility; diff --git a/frontend/app/components/ui/Toggler/toggler.css b/frontend/app/components/ui/Toggler/toggler.css index cf9cf0838..6d5fae271 100644 --- a/frontend/app/components/ui/Toggler/toggler.css +++ b/frontend/app/components/ui/Toggler/toggler.css @@ -39,12 +39,10 @@ width: 20px; left: 0; bottom: -2px; - /* 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); } @@ -55,6 +53,7 @@ .slider.checked:before { border: solid 1px $teal; + background-color: $teal !important; transform: translateX(15px); } diff --git a/frontend/app/svg/icons/binoculars.svg b/frontend/app/svg/icons/binoculars.svg new file mode 100644 index 000000000..47bca444f --- /dev/null +++ b/frontend/app/svg/icons/binoculars.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 0d00cf0349fa8edd179bac80a95f00a70db963d5 Mon Sep 17 00:00:00 2001 From: sylenien Date: Thu, 12 May 2022 16:34:11 +0200 Subject: [PATCH 05/11] more search field fixes --- .../Filters/FilterModal/FilterModal.tsx | 17 +++++--- .../LiveFilterModal/LiveFilterModal.tsx | 39 +++++++++++++++++-- .../components/DefaultTimezone.tsx | 13 +++++-- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx index 04688de6b..a11695d5c 100644 --- a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx +++ b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx @@ -15,7 +15,7 @@ interface Props { searchQuery?: string, } function FilterModal(props: Props) { - const { + const { filters, metaOptions, onFilterClick = () => null, @@ -32,13 +32,20 @@ function FilterModal(props: Props) { _filter.value = [filter.value]; onFilterClick(_filter); } - + + const isResultEmpty = !filterSearchList || Object.keys(filterSearchList).length === 0 return (
+ { showSearchList && (
- { filterSearchList && Object.keys(filterSearchList).map((key, index) => { + {isResultEmpty && !fetchingFilterSearchList ? ( +
+ +
No Suggestions Found
+
+ ) : Object.keys(filterSearchList).map((key, index) => { const filter = filterSearchList[key]; const option = filtersMap[key]; return option ? ( @@ -65,7 +72,7 @@ function FilterModal(props: Props) {
)} - + { !hasSearchQuery && (
{filters && Object.keys(filters).map((key) => ( @@ -92,4 +99,4 @@ export default connect(state => ({ filterSearchList: state.getIn([ 'search', 'filterSearchList' ]), metaOptions: state.getIn([ 'customFields', 'list' ]), fetchingFilterSearchList: state.getIn([ 'search', 'fetchFilterSearch', 'loading' ]), -}))(FilterModal); \ No newline at end of file +}))(FilterModal); diff --git a/frontend/app/components/shared/Filters/LiveFilterModal/LiveFilterModal.tsx b/frontend/app/components/shared/Filters/LiveFilterModal/LiveFilterModal.tsx index c75d0f4ca..b00897add 100644 --- a/frontend/app/components/shared/Filters/LiveFilterModal/LiveFilterModal.tsx +++ b/frontend/app/components/shared/Filters/LiveFilterModal/LiveFilterModal.tsx @@ -15,7 +15,7 @@ interface Props { searchQuery?: string, } function LiveFilterModal(props: Props) { - const { + const { filters, metaOptions, onFilterClick = () => null, @@ -32,7 +32,9 @@ function LiveFilterModal(props: Props) { _filter.value = [filter.value]; onFilterClick(_filter); } - + + const isResultEmpty = !filterSearchList || Object.keys(filterSearchList).filter(i => filtersMap[i].isLive).length === 0 + return (
{ showSearchList && ( @@ -62,10 +64,39 @@ function LiveFilterModal(props: Props) {
); })} + {isResultEmpty && !fetchingFilterSearchList ? ( +
+ +
No Suggestions Found
+
+ ) : Object.keys(filterSearchList).filter(i => filtersMap[i].isLive).map((key, index) => { + const filter = filterSearchList[key]; + const option = filtersMap[key]; + return ( +
+
{option.label}
+
+ {filter.map((f, i) => ( +
onFilterSearchClick({ type: key, value: f.value })} + > + +
{f.value}
+
+ ))} +
+
+ ); + })}
)} - + { !hasSearchQuery && (
{filters && Object.keys(filters).map((key) => ( @@ -92,4 +123,4 @@ export default connect(state => ({ filterSearchList: state.getIn([ 'search', 'filterSearchList' ]), metaOptions: state.getIn([ 'customFields', 'list' ]), fetchingFilterSearchList: state.getIn([ 'search', 'fetchFilterSearch', 'loading' ]), -}))(LiveFilterModal); \ No newline at end of file +}))(LiveFilterModal); diff --git a/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx b/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx index efb91fed9..8e6edee88 100644 --- a/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx +++ b/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx @@ -6,8 +6,15 @@ import { useObserver } from 'mobx-react-lite'; const str = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/) const d = str && str[1] || 'UTC'; + +const localMachineFormat = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/) +const middlePoint = localMachineFormat && localMachineFormat[1].length - 2 +const readableLocalTimezone = localMachineFormat && middlePoint ? + `${localMachineFormat[1].substring(0, 3)} ${localMachineFormat[1].substring(3, middlePoint)}:${localMachineFormat[1].substring(middlePoint)}` + : null + const timezoneOptions = [ - { label: d, value: 'local' }, + { label: readableLocalTimezone, value: 'local' }, { label: 'UTC', value: 'UTC' }, ] @@ -38,9 +45,9 @@ function DefaultTimezone(props) { }}>Update
-
This change will impact the timestamp on session card and player.
+
This change will impact the timestamp on session card and player.
); } -export default DefaultTimezone; \ No newline at end of file +export default DefaultTimezone; From be13ff5f7a6768b3b748cd9a12137451ca4e3c5a Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 13 May 2022 13:32:01 +0200 Subject: [PATCH 06/11] fix(ui): fixed sessionitem and timezone dropdown connection to mobx --- .../shared/SessionItem/SessionItem.js | 162 ----------------- .../shared/SessionItem/SessionItem.tsx | 163 ++++++++++-------- .../shared/SessionItem/{index.js => index.ts} | 0 .../shared/SessionItem/sessionItem.css | 20 ++- .../components/DefaultTimezone.tsx | 47 +++-- frontend/app/date.ts | 20 ++- 6 files changed, 155 insertions(+), 257 deletions(-) delete mode 100644 frontend/app/components/shared/SessionItem/SessionItem.js rename frontend/app/components/shared/SessionItem/{index.js => index.ts} (100%) diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js deleted file mode 100644 index 6222327f3..000000000 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ /dev/null @@ -1,162 +0,0 @@ -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 PlayLink from './PlayLink'; -import ErrorBars from './ErrorBars'; -import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from "App/routes"; -import { capitalize } from 'App/utils'; - -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 -export default class SessionItem extends React.PureComponent { - // eslint-disable-next-line complexity - 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, - } = this.props; - const formattedDuration = durationFormatted(duration); - const hasUserId = userId || userAnonymousId; - const isSessions = isRoute(SESSIONS_ROUTE, this.props.location.pathname); - const isAssist = isRoute(ASSIST_ROUTE, this.props.location.pathname) || isRoute(ASSIST_LIVE_SESSION, this.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 ( -
-
-
-
-
-
-
(!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)} - > - -
-
-
-
-
{formatTimeOrDate(startedAt, timezone) }
-
- {!isAssist && ( - <> -
- { eventsCount } - { eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' } -
-
·
- - )} -
{ live ? : formattedDuration }
-
-
-
- -
- - - -
·
- - - -
·
- - - -
-
- { isSessions && ( -
- -
- )} -
- -
- { isAssist && showActive && ( - - )} -
- { isSessions && ( -
- { isLastPlayed && ( - - )} -
- )} - -
-
-
- { _metaList.length > 0 && ( - - )} -
- ); - } -} \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index dc51bfc17..f00f58c82 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -1,90 +1,112 @@ -import React from 'react'; -import { connect } from 'react-redux'; +import React from 'react' import cn from 'classnames'; -import { - Link, - Icon, +import { CountryFlag, Avatar, TextEllipsis, Label, } from 'UI'; -import { toggleFavorite, setSessionPath } from 'Duck/sessions'; -import { session as sessionRoute, liveSession as liveSessionRoute, withSiteId } from 'App/routes'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; import { durationFormatted, formatTimeOrDate } from 'App/date'; import stl from './sessionItem.css'; import Counter from './Counter' -import { withRouter } from 'react-router-dom'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; import SessionMetaList from './SessionMetaList'; +import PlayLink from './PlayLink'; 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; +interface Props { + session: { + sessionId: string; + userBrowser: string; + userOs: string; + userId: string; + userAnonymousId: string; + userDisplayName: string; + userCountry: string; + startedAt: number; + duration: string; + eventsCount: number; + errorsCount: number; + pagesCount: number; + viewed: boolean; + favorite: boolean; + userDeviceType: string; + userUuid: string; + userNumericHash: number; + live: boolean + metadata: Record; + userSessionsCount: number + issueTypes: []; + active: boolean; + onUserClick: (userId: string, userAnonymousId: string) => null; + hasUserFilter: boolean; + disableUser: boolean; + metaList: Array; + showActive: boolean; + lastPlayedSessionId: string; + }, +} - const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => { - const value = metadata[key]; - return { label: key, value }; - }); +function SessionItem(props: RouteComponentProps) { + const { settingsStore } = useStore(); + const { timezone } = settingsStore.sessionSettings; - return ( -
+ const { + session: { + sessionId, + userBrowser, + userOs, + userId, + userAnonymousId, + userDisplayName, + userCountry, + startedAt, + duration, + eventsCount, + viewed, + userDeviceType, + userNumericHash, + live, + metadata, + issueTypes, + active, + }, + 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 ( +
(!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)} > @@ -111,7 +133,7 @@ function SessionItem(props) {
- +
·
@@ -145,9 +167,11 @@ function SessionItem(props) { )}
)} - - - +
@@ -155,10 +179,7 @@ function SessionItem(props) { )}
- ); - } + ) +} -export default connect(state => ({ - timezone: localStorage.getItem(TIMEZONE) || '', - siteId: state.getIn([ 'site', 'siteId' ]), -}), { toggleFavorite, setSessionPath })(withRouter(SessionItem)); \ No newline at end of file +export default withRouter(observer(SessionItem)) diff --git a/frontend/app/components/shared/SessionItem/index.js b/frontend/app/components/shared/SessionItem/index.ts similarity index 100% rename from frontend/app/components/shared/SessionItem/index.js rename to frontend/app/components/shared/SessionItem/index.ts diff --git a/frontend/app/components/shared/SessionItem/sessionItem.css b/frontend/app/components/shared/SessionItem/sessionItem.css index cb7b87c3a..897ee327f 100644 --- a/frontend/app/components/shared/SessionItem/sessionItem.css +++ b/frontend/app/components/shared/SessionItem/sessionItem.css @@ -1,11 +1,9 @@ -@import 'mixins.css'; - .sessionItem { background-color: #fff; - @mixin defaultHover; user-select: none; border-radius: 3px; border: solid thin #EEEEEE; + transition: all 0.4s; & .favorite { opacity: 0; @@ -15,6 +13,10 @@ } &:hover { + background-color: $active-blue; + border: solid thin $active-blue-border; + transition: all 0.2s; + & .playLink { transition: all 0.4s; opacity: 1; @@ -103,7 +105,11 @@ letter-spacing: 1px; } -.userName:hover { - text-decoration: underline; - text-decoration-color: $teal; -} \ No newline at end of file +.userName { + text-decoration: none; + + &:hover { + text-decoration: underline; + text-decoration-color: $teal; + } +} diff --git a/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx b/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx index 8e6edee88..1f1299025 100644 --- a/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx +++ b/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx @@ -5,24 +5,49 @@ 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 localMachineFormat = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/) -const middlePoint = localMachineFormat && localMachineFormat[1].length - 2 -const readableLocalTimezone = localMachineFormat && middlePoint ? - `${localMachineFormat[1].substring(0, 3)} ${localMachineFormat[1].substring(3, middlePoint)}:${localMachineFormat[1].substring(middlePoint)}` - : null +interface TimezonesDropdownValue { + label: string; + value: string; +} +type TimezonesDropdown = TimezonesDropdownValue[] -const timezoneOptions = [ - { label: readableLocalTimezone, value: 'local' }, - { label: 'UTC', value: 'UTC' }, -] +const generateGMTZones = (): TimezonesDropdown => { + const timezones: TimezonesDropdown = [] + + const positiveNumbers = [...Array(12).keys()]; + const negativeNumbers = [...Array(12).keys()].reverse(); + negativeNumbers.pop(); // remove trailing zero since we have on in positive numbers array + + const combinedArray = [...negativeNumbers, ...positiveNumbers]; + + for (let i = 0; i < 23; i++) { + let symbol = i < 11 ? '-' : '+'; + let isUTC = i === 11 + let prefix = isUTC ? 'UTC / GMT' : 'GMT'; + let value = String(combinedArray[i]).padStart(2, '0'); + + let tz = `${prefix} ${symbol}${String(combinedArray[i]).padStart(2, '0')}:00` + + let dropdownValue = `UTC${symbol}${value}` + timezones.push({ label: tz, value: isUTC ? 'UTC' : dropdownValue }) + } + + timezones.splice(17, 0, { label: 'GMT +05:30', value: 'GMT +05:30' }) + return timezones +} + +const timezoneOptions: TimezonesDropdown = [...generateGMTZones()] 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) + const sessionSettings = useObserver(() => settingsStore.sessionSettings); + + useEffect(() => { + if (!timezone) setTimezone('local'); + }, []); return ( <> diff --git a/frontend/app/date.ts b/frontend/app/date.ts index 8a9501a86..e32704db1 100644 --- a/frontend/app/date.ts +++ b/frontend/app/date.ts @@ -20,7 +20,7 @@ export const durationFormatted = (duration: Duration):string => { export function durationFromMsFormatted(ms: number): string { return durationFormatted(Duration.fromMillis(ms || 0)); -} +} export const durationFormattedFull = (duration: Duration): string => { if (duration.as('minutes') < 1) { // show in seconds @@ -35,7 +35,7 @@ export const durationFormattedFull = (duration: Duration): string => { } else if (duration.as('months') < 1) { // show in days and hours let d = duration.toFormat('d'); duration = d + (d > 1 ? ' days' : ' day'); - } else { + } else { let d = Math.trunc(duration.as('months')); duration = d + (d > 1 ? ' months' : ' month');; } @@ -49,7 +49,7 @@ export const msToSec = (ms:number): number => Math.round(ms / 1000); export const diffFromNowString = (ts:number): string => durationFormattedFull(DateTime.fromMillis(Date.now()).diff(DateTime.fromMillis(ts))); -export const diffFromNowShortString = (ts: number): string => +export const diffFromNowShortString = (ts: number): string => durationFormatted(DateTime.fromMillis(Date.now()).diff(DateTime.fromMillis(ts))); export const getDateFromMill = date => @@ -69,11 +69,19 @@ export function formatDateTimeDefault(timestamp: number): string { return isToday(date) ? 'Today' : date.toFormat('LLL dd, yyyy') + ', ' + date.toFormat('hh:mm a') } +/** + * Formats timestamps into readable date + * @param {Number} timestamp + * @param {String} timezone fixed offset like UTC+6 + * @returns {String} formatted date (or time if its today) + */ export function formatTimeOrDate(timestamp: number, timezone: string): string { var date = DateTime.fromMillis(timestamp) - if (timezone === 'UTC') - date = date.toUTC(); - + if (timezone) { + if (timezone === 'UTC') date = date.toUTC(); + date = date.setZone(timezone) + } + return isToday(date) ? date.toFormat('hh:mm a') : date.toFormat('LLL dd, yyyy, hh:mm a'); } From 19178807f8cc341cc14f087c1daad487534dd2d9 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 13 May 2022 13:35:27 +0200 Subject: [PATCH 07/11] fix(ui): fixed sessionitem types and removed withrouter connection --- .../components/shared/SessionItem/SessionItem.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index f00f58c82..d8e485fbb 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -11,7 +11,7 @@ import { observer } from 'mobx-react-lite'; import { durationFormatted, formatTimeOrDate } from 'App/date'; import stl from './sessionItem.css'; import Counter from './Counter' -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { useLocation, RouteComponentProps } from 'react-router-dom'; import SessionMetaList from './SessionMetaList'; import PlayLink from './PlayLink'; import ErrorBars from './ErrorBars'; @@ -55,7 +55,7 @@ interface Props { }, } -function SessionItem(props: RouteComponentProps) { +function SessionItem(props: Props) { const { settingsStore } = useStore(); const { timezone } = settingsStore.sessionSettings; @@ -87,10 +87,14 @@ function SessionItem(props: RouteComponentProps) { lastPlayedSessionId, } = props; + const location = useLocation(); + + console.log(location.pathname); + 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 isSessions = isRoute(SESSIONS_ROUTE, location.pathname); + const isAssist = isRoute(ASSIST_ROUTE, location.pathname) || isRoute(ASSIST_LIVE_SESSION, location.pathname); const isLastPlayed = lastPlayedSessionId === sessionId; const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => { @@ -182,4 +186,4 @@ function SessionItem(props: RouteComponentProps) { ) } -export default withRouter(observer(SessionItem)) +export default observer(SessionItem) From e7a31dbb8c8f00b9f941e31d345b39198ce7a6d2 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 13 May 2022 13:39:49 +0200 Subject: [PATCH 08/11] fix(ui): refactor sessionitem --- .../shared/SessionItem/SessionItem.tsx | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index d8e485fbb..09eae802f 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -46,13 +46,13 @@ interface Props { userSessionsCount: number issueTypes: []; active: boolean; - onUserClick: (userId: string, userAnonymousId: string) => null; - hasUserFilter: boolean; - disableUser: boolean; - metaList: Array; - showActive: boolean; - lastPlayedSessionId: string; }, + onUserClick: (userId: string, userAnonymousId: string) => null; + hasUserFilter: boolean; + disableUser: boolean; + metaList: Array; + showActive: boolean; + lastPlayedSessionId: string; } function SessionItem(props: Props) { @@ -60,25 +60,7 @@ function SessionItem(props: Props) { const { timezone } = settingsStore.sessionSettings; const { - session: { - sessionId, - userBrowser, - userOs, - userId, - userAnonymousId, - userDisplayName, - userCountry, - startedAt, - duration, - eventsCount, - viewed, - userDeviceType, - userNumericHash, - live, - metadata, - issueTypes, - active, - }, + session, onUserClick = () => null, hasUserFilter = false, disableUser = false, @@ -87,6 +69,25 @@ function SessionItem(props: Props) { lastPlayedSessionId, } = props; + const { + sessionId, + userBrowser, + userOs, + userId, + userAnonymousId, + userDisplayName, + userCountry, + startedAt, + duration, + eventsCount, + viewed, + userDeviceType, + userNumericHash, + live, + metadata, + issueTypes, + active, + } = session; const location = useLocation(); console.log(location.pathname); From cea121861367bc8a2ac563a4f1f82d56880bb775 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 13 May 2022 13:55:01 +0200 Subject: [PATCH 09/11] fix(ui): fix typo in comment --- .../shared/SessionSettings/components/DefaultTimezone.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx b/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx index 1f1299025..2332b62da 100644 --- a/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx +++ b/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx @@ -17,7 +17,7 @@ const generateGMTZones = (): TimezonesDropdown => { const positiveNumbers = [...Array(12).keys()]; const negativeNumbers = [...Array(12).keys()].reverse(); - negativeNumbers.pop(); // remove trailing zero since we have on in positive numbers array + negativeNumbers.pop(); // remove trailing zero since we have one in positive numbers array const combinedArray = [...negativeNumbers, ...positiveNumbers]; From d72f47b2964a6c491aadf3b1398fe46510e63b58 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 13 May 2022 15:35:22 +0200 Subject: [PATCH 10/11] fix(ui): fix prop types for sessionitem --- .../LiveSessionList/LiveSessionList.tsx | 14 ++++++------ .../shared/SessionItem/SessionItem.tsx | 22 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx index b0dbd80ac..ab6f0c662 100644 --- a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx +++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx @@ -20,14 +20,14 @@ const PER_PAGE = 10; interface Props { loading: Boolean, - list: List, + list: List, fetchLiveList: () => Promise, applyFilter: () => void, filters: any, addAttribute: (obj) => void, addFilterByKeyAndValue: (key: FilterKey, value: string) => void, updateCurrentPage: (page: number) => void, - currentPage: number, + currentPage: number, metaList: any, updateSort: (sort: any) => void, sort: any, @@ -41,7 +41,7 @@ function LiveSessionList(props: Props) { const sortOptions = metaList.map(i => ({ text: capitalize(i), value: i })).toJS(); - + // const displayedCount = Math.min(currentPage * PER_PAGE, sessions.size); // const addPage = () => props.updateCurrentPage(props.currentPage + 1) @@ -69,7 +69,7 @@ function LiveSessionList(props: Props) { if (filter.key === FilterKey.USERID) { const _userId = session.userId ? session.userId.toLowerCase() : ''; hasValidFilter = _values.length > 0 ? (_values.includes(_userId) && hasValidFilter) || _values.some(i => _userId.includes(i)) : hasValidFilter; - } + } if (filter.category === FilterCategory.METADATA) { const _source = session.metadata[filter.key] ? session.metadata[filter.key].toLowerCase() : ''; hasValidFilter = _values.length > 0 ? (_values.includes(_source) && hasValidFilter) || _values.some(i => _source.includes(i)) : hasValidFilter; @@ -80,7 +80,7 @@ function LiveSessionList(props: Props) { setSessions(filteredSessions); }, [filters, list]); - useEffect(() => { + useEffect(() => { props.fetchLiveList(); timeout(); return () => { @@ -88,7 +88,7 @@ function LiveSessionList(props: Props) { } }, []) - const onUserClick = (userId, userAnonymousId) => { + const onUserClick = (userId: string, userAnonymousId: string) => { if (userId) { props.addFilterByKeyAndValue(FilterKey.USERID, userId); } else { @@ -183,7 +183,7 @@ export default withPermissions(['ASSIST_LIVE'])(connect( metaList: state.getIn(['customFields', 'list']).map(i => i.key), sort: state.getIn(['liveSearch', 'sort']), }), - { + { fetchLiveList, applyFilter, addAttribute, diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index 09eae802f..7129a04c0 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -11,7 +11,7 @@ import { observer } from 'mobx-react-lite'; import { durationFormatted, formatTimeOrDate } from 'App/date'; import stl from './sessionItem.css'; import Counter from './Counter' -import { useLocation, RouteComponentProps } from 'react-router-dom'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; import SessionMetaList from './SessionMetaList'; import PlayLink from './PlayLink'; import ErrorBars from './ErrorBars'; @@ -47,15 +47,16 @@ interface Props { issueTypes: []; active: boolean; }, - onUserClick: (userId: string, userAnonymousId: string) => null; - hasUserFilter: boolean; - disableUser: boolean; - metaList: Array; - showActive: boolean; - lastPlayedSessionId: string; + onUserClick?: (userId: string, userAnonymousId: string) => void; + hasUserFilter?: boolean; + disableUser?: boolean; + metaList?: Array; + showActive?: boolean; + lastPlayedSessionId?: string; + live?: boolean; } -function SessionItem(props: Props) { +function SessionItem(props: RouteComponentProps) { const { settingsStore } = useStore(); const { timezone } = settingsStore.sessionSettings; @@ -88,9 +89,8 @@ function SessionItem(props: Props) { issueTypes, active, } = session; - const location = useLocation(); - console.log(location.pathname); + const location = props.location; const formattedDuration = durationFormatted(duration); const hasUserId = userId || userAnonymousId; @@ -187,4 +187,4 @@ function SessionItem(props: Props) { ) } -export default observer(SessionItem) +export default withRouter(observer(SessionItem)) From 58397e6c6c7188d49af72a2d662747dbde0a52cd Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 13 May 2022 15:36:24 +0200 Subject: [PATCH 11/11] fix(ui): remove attrs from icons --- frontend/app/svg/icons/binoculars.svg | 4 ++-- frontend/app/svg/icons/filters/arrow-return-right.svg | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/app/svg/icons/binoculars.svg b/frontend/app/svg/icons/binoculars.svg index 47bca444f..e85d493b3 100644 --- a/frontend/app/svg/icons/binoculars.svg +++ b/frontend/app/svg/icons/binoculars.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/frontend/app/svg/icons/filters/arrow-return-right.svg b/frontend/app/svg/icons/filters/arrow-return-right.svg index 228e24b4a..bbdcd41f7 100644 --- a/frontend/app/svg/icons/filters/arrow-return-right.svg +++ b/frontend/app/svg/icons/filters/arrow-return-right.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file +