fix ui - design issues fixes (#1995)
This commit is contained in:
parent
d7b79475a4
commit
d0ec589ecb
5 changed files with 259 additions and 125 deletions
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
function InsightItem(props: Props) {
|
||||
const { item, onClick = () => {} } = props;
|
||||
const className =
|
||||
'flex items-start flex-wrap py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer';
|
||||
'flex items-start py-3 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer';
|
||||
|
||||
switch (item.category) {
|
||||
case IssueCategory.RAGE:
|
||||
|
|
@ -52,7 +52,7 @@ function ErrorItem({ item, className, onClick }: any) {
|
|||
<div className={className} onClick={onClick}>
|
||||
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
|
||||
{item.isNew ? (
|
||||
<div className="flex items-center gap-2 whitespace-nowrap">
|
||||
<div className="flex items-center gap-1 flex-wrap whitespace-nowrap">
|
||||
<div>Users are encountering a new error called:</div>
|
||||
<div className="bg-gray-100 px-2 rounded">{item.name}</div>
|
||||
<div>This error has occurred a total of</div>
|
||||
|
|
@ -60,7 +60,7 @@ function ErrorItem({ item, className, onClick }: any) {
|
|||
<div>times</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 whitespace-nowrap">
|
||||
<div className="flex items-center gap-1 flex-wrap whitespace-nowrap">
|
||||
<div>There has been an</div>
|
||||
<div>{item.isIncreased ? 'increase' : 'decrease'}</div>
|
||||
<div>in the error</div>
|
||||
|
|
@ -82,7 +82,7 @@ function NetworkItem({ item, className, onClick }: any) {
|
|||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<div>Network request to path</div>
|
||||
<div className="bg-gray-100 px-2 rounded">{item.name}</div>
|
||||
<div>has {item.change > 0 ? 'increased' : 'decreased'}</div>
|
||||
|
|
@ -96,7 +96,7 @@ function ResourcesItem({ item, className, onClick }: any) {
|
|||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<div>There has been</div>
|
||||
<div>{item.change > 0 ? 'Increase' : 'Decrease'}</div>
|
||||
<div>in</div>
|
||||
|
|
@ -113,14 +113,14 @@ function RageItem({ item, className, onClick }: any) {
|
|||
<div className={className} onClick={onClick}>
|
||||
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
|
||||
{item.isNew ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<div>New Click Rage detected</div>
|
||||
<div className="mx-1 bg-gray-100 px-2 rounded">{item.value}</div>
|
||||
<div>times on</div>
|
||||
<div className="mx-1 bg-gray-100 px-2 rounded">{item.name}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<div>Click rage has</div>
|
||||
<div>{item.isIncreased ? 'increased' : 'decreased'} on</div>
|
||||
<div className="mx-1 bg-gray-100 px-2 rounded">{item.name}</div>
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ export function SummaryButton({
|
|||
);
|
||||
}
|
||||
|
||||
const gradientButton = {
|
||||
export const gradientButton = {
|
||||
border: 'double 1px transparent',
|
||||
borderRadius: '60px',
|
||||
background:
|
||||
|
|
|
|||
|
|
@ -1,19 +1,28 @@
|
|||
import { CloseOutlined, EnterOutlined } from '@ant-design/icons';
|
||||
import { Tour } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Input, Icon } from 'UI';
|
||||
import FilterModal from 'Shared/Filters/FilterModal';
|
||||
import { debounce } from 'App/utils';
|
||||
|
||||
import { useStore } from 'App/mstore';
|
||||
import { assist as assistRoute, isRoute } from 'App/routes';
|
||||
import { addFilterByKeyAndValue, fetchFilterSearch, edit, clearSearch } from 'Duck/search';
|
||||
import { debounce } from 'App/utils';
|
||||
import {
|
||||
addFilterByKeyAndValue as liveAddFilterByKeyAndValue,
|
||||
fetchFilterSearch as liveFetchFilterSearch,
|
||||
} from 'Duck/liveSearch';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Segmented } from 'antd';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import { EnterOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
addFilterByKeyAndValue,
|
||||
clearSearch,
|
||||
edit,
|
||||
fetchFilterSearch,
|
||||
} from 'Duck/search';
|
||||
import { Icon, Input, Toggler as Switch } from 'UI';
|
||||
|
||||
import FilterModal from 'Shared/Filters/FilterModal';
|
||||
|
||||
import { SwitchToggle } from '../../ui/Toggler/Toggler';
|
||||
import OutsideClickDetectingDiv from '../OutsideClickDetectingDiv';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
|
||||
|
|
@ -32,7 +41,10 @@ function SessionSearchField(props: Props) {
|
|||
isRoute(ASSIST_ROUTE, window.location.pathname) ||
|
||||
window.location.pathname.includes('multiview');
|
||||
const debounceFetchFilterSearch = React.useCallback(
|
||||
debounce(isLive ? props.liveFetchFilterSearch : props.fetchFilterSearch, 1000),
|
||||
debounce(
|
||||
isLive ? props.liveFetchFilterSearch : props.fetchFilterSearch,
|
||||
1000
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
|
|
@ -64,12 +76,14 @@ function SessionSearchField(props: Props) {
|
|||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onChange={onSearchChange}
|
||||
placeholder={'Search sessions using any captured event (click, input, page, error...)'}
|
||||
style={{ minWidth: 360 }}
|
||||
placeholder={
|
||||
'Search sessions using any captured event (click, input, page, error...)'
|
||||
}
|
||||
style={{ minWidth: 360, height: 33 }}
|
||||
id="search"
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
className="text-lg placeholder-lg !border-0 w-full rounded-r-lg focus:!border-0 focus:ring-0"
|
||||
className="px-2 py-1 text-lg placeholder-lg !border-0 rounded-r-lg focus:!border-0 focus:ring-0"
|
||||
/>
|
||||
|
||||
{showModal && (
|
||||
|
|
@ -86,126 +100,217 @@ function SessionSearchField(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const AiSearchField = observer(({ edit, appliedFilter, clearSearch }: Props) => {
|
||||
const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0;
|
||||
const { aiFiltersStore } = useStore();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const debounceAiFetch = React.useCallback(debounce(aiFiltersStore.getSearchFilters, 1000), []);
|
||||
const AiSearchField = observer(
|
||||
({ edit, appliedFilter, clearSearch }: Props) => {
|
||||
const hasFilters =
|
||||
appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0;
|
||||
const { aiFiltersStore } = useStore();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const debounceAiFetch = React.useCallback(
|
||||
debounce(aiFiltersStore.getSearchFilters, 1000),
|
||||
[]
|
||||
);
|
||||
|
||||
const onSearchChange = ({ target: { value } }: any) => {
|
||||
setSearchQuery(value);
|
||||
};
|
||||
const onSearchChange = ({ target: { value } }: any) => {
|
||||
setSearchQuery(value);
|
||||
};
|
||||
|
||||
const fetchResults = () => {
|
||||
if (searchQuery) {
|
||||
debounceAiFetch(searchQuery);
|
||||
}
|
||||
};
|
||||
const fetchResults = () => {
|
||||
if (searchQuery) {
|
||||
debounceAiFetch(searchQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
fetchResults();
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
fetchResults();
|
||||
}
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
clearSearch();
|
||||
setSearchQuery('');
|
||||
};
|
||||
const clearAll = () => {
|
||||
clearSearch();
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (aiFiltersStore.filtersSetKey !== 0) {
|
||||
console.log('updating filters', aiFiltersStore.filters, aiFiltersStore.filtersSetKey);
|
||||
edit(aiFiltersStore.filters);
|
||||
}
|
||||
}, [aiFiltersStore.filters, aiFiltersStore.filtersSetKey]);
|
||||
React.useEffect(() => {
|
||||
if (aiFiltersStore.filtersSetKey !== 0) {
|
||||
edit(aiFiltersStore.filters);
|
||||
}
|
||||
}, [aiFiltersStore.filters, aiFiltersStore.filtersSetKey]);
|
||||
|
||||
return (
|
||||
<div className={'w-full'}>
|
||||
<Input
|
||||
onChange={onSearchChange}
|
||||
placeholder={'E.g., "Sessions with login issues this week"'}
|
||||
id="search"
|
||||
onKeyDown={handleKeyDown}
|
||||
value={searchQuery}
|
||||
autoComplete="off"
|
||||
className="text-lg placeholder-lg !border-0 rounded-r-lg focus:!border-0 focus:ring-0"
|
||||
leadingButton={
|
||||
searchQuery !== '' ? (
|
||||
<div
|
||||
className={'h-full flex items-center cursor-pointer'}
|
||||
onClick={hasFilters ? clearAll : fetchResults}
|
||||
>
|
||||
<div className={'px-2 py-1 hover:bg-active-blue rounded mr-2'}>
|
||||
{hasFilters ? <CloseOutlined /> : <EnterOutlined />}
|
||||
return (
|
||||
<div className={'w-full'}>
|
||||
<Input
|
||||
onChange={onSearchChange}
|
||||
placeholder={'E.g., "Sessions with login issues this week"'}
|
||||
id="search"
|
||||
onKeyDown={handleKeyDown}
|
||||
value={searchQuery}
|
||||
style={{ minWidth: 360, height: 33 }}
|
||||
autoComplete="off"
|
||||
className="px-2 py-1 text-lg placeholder-lg !border-0 rounded-r-lg focus:!border-0 focus:ring-0"
|
||||
leadingButton={
|
||||
searchQuery !== '' ? (
|
||||
<div
|
||||
className={'h-full flex items-center cursor-pointer'}
|
||||
onClick={hasFilters ? clearAll : fetchResults}
|
||||
>
|
||||
<div className={'px-2 py-1 hover:bg-active-blue rounded mr-2'}>
|
||||
{hasFilters ? <CloseOutlined /> : <EnterOutlined />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function AiSessionSearchField(props: Props) {
|
||||
const [tab, setTab] = useState('search');
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const askTourKey = '__or__ask-tour';
|
||||
const tabKey = '__or__tab';
|
||||
const { aiFiltersStore } = useStore();
|
||||
const isTourShown = localStorage.getItem(askTourKey) !== null;
|
||||
const [tab, setTab] = useState(localStorage.getItem(tabKey) || 'search');
|
||||
const [touring, setTouring] = useState(!isTourShown);
|
||||
const askAiRef = React.useRef(null);
|
||||
|
||||
const boxStyle = isFocused ? gradientBox : gradientBoxUnfocused;
|
||||
const closeTour = () => {
|
||||
setTouring(false);
|
||||
localStorage.setItem(askTourKey, 'true');
|
||||
};
|
||||
const changeValue = (v?: string) => {
|
||||
const newTab = v ? v : tab !== 'ask' ? 'ask' : 'search';
|
||||
setTab(newTab);
|
||||
localStorage.setItem(tabKey, newTab);
|
||||
};
|
||||
return (
|
||||
<OutsideClickDetectingDiv
|
||||
onClickOutside={() => setIsFocused(false)}
|
||||
className={'bg-white rounded-lg'}
|
||||
>
|
||||
<div style={boxStyle} onClick={() => setIsFocused(true)}>
|
||||
<Segmented
|
||||
value={tab}
|
||||
// className={'bg-figmaColors-divider'}
|
||||
onChange={(value) => setTab(value as string)}
|
||||
options={[
|
||||
<div className={'bg-white rounded-lg'}>
|
||||
<div style={gradientBoxUnfocused}>
|
||||
<div ref={askAiRef} className={'px-2'}>
|
||||
<AskAiSwitchToggle
|
||||
enabled={tab === 'ask'}
|
||||
setEnabled={changeValue}
|
||||
loading={aiFiltersStore.isLoading}
|
||||
/>
|
||||
</div>
|
||||
{tab === 'ask' ? (
|
||||
<AiSearchField {...props} />
|
||||
) : (
|
||||
<SessionSearchField {...props} />
|
||||
)}
|
||||
<Tour
|
||||
open={touring}
|
||||
onClose={closeTour}
|
||||
steps={[
|
||||
{
|
||||
label: (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<Icon name={'search'} size={16} />
|
||||
<span>Search</span>
|
||||
</div>
|
||||
),
|
||||
value: 'search',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<Icon name={'sparkles'} size={16} />
|
||||
title: (
|
||||
<div
|
||||
className={'text-xl font-semibold flex items-center gap-2'}
|
||||
>
|
||||
<span>Introducing</span>
|
||||
<Icon name={'sparkles'} size={18} />
|
||||
<span>Ask AI</span>
|
||||
</div>
|
||||
),
|
||||
value: 'ask',
|
||||
target: () => askAiRef.current,
|
||||
description:
|
||||
'Easily find sessions with our AI search. Just enable Ask AI, type in your query naturally, and the AI will swiftly and precisely display relevant sessions.',
|
||||
nextButtonProps: {
|
||||
children: (
|
||||
<OutsideClickDetectingDiv
|
||||
onClickOutside={closeTour}
|
||||
className={
|
||||
'w-full h-full text-white flex items-center gap-2'
|
||||
}
|
||||
>
|
||||
<span>Ask AI</span>
|
||||
<Icon
|
||||
name={'arrow-right-short'}
|
||||
size={16}
|
||||
color={'white'}
|
||||
/>
|
||||
</OutsideClickDetectingDiv>
|
||||
),
|
||||
onClick: () => {
|
||||
changeValue('tab');
|
||||
closeTour();
|
||||
},
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{tab === 'ask' ? <AiSearchField {...props} /> : <SessionSearchField {...props} />}
|
||||
</div>
|
||||
</OutsideClickDetectingDiv>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const gradientBox = {
|
||||
border: 'double 1px transparent',
|
||||
borderRadius: '6px',
|
||||
background:
|
||||
'linear-gradient(#f6f6f6, #f6f6f6), linear-gradient(to right, #394EFF 0%, #3EAAAF 100%)',
|
||||
backgroundOrigin: 'border-box',
|
||||
backgroundClip: 'content-box, border-box',
|
||||
display: 'flex',
|
||||
gap: '0.25rem',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
export const AskAiSwitchToggle = ({
|
||||
enabled,
|
||||
setEnabled,
|
||||
loading,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
loading: boolean;
|
||||
setEnabled: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
onClick={() => setEnabled()}
|
||||
className={loading ? 'animate-bg-spin' : ''}
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
height: 24,
|
||||
background: enabled
|
||||
? 'linear-gradient(-25deg, #394eff, #3EAAAf, #3ccf65)'
|
||||
: 'rgb(170 170 170)',
|
||||
backgroundSize: loading ? '200% 200%' : 'unset',
|
||||
borderRadius: 100,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
border: 0,
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
insetInlineStart: enabled ? 'calc(100% - 21px)' : '3px',
|
||||
position: 'absolute',
|
||||
top: 3,
|
||||
width: 18,
|
||||
height: 18,
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
background: '#fff',
|
||||
borderRadius: 100,
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 100,
|
||||
height: '100%',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
paddingInline: !enabled ? '30px 0px' : '10px 24px',
|
||||
width: 88,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: 'white', fontSize: 16 }}>Ask AI</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const gradientBoxUnfocused = {
|
||||
borderRadius: '6px',
|
||||
border: 'double 1px transparent',
|
||||
background: '#f6f6f6',
|
||||
background: 'white',
|
||||
display: 'flex',
|
||||
gap: '0.25rem',
|
||||
alignItems: 'center',
|
||||
|
|
|
|||
|
|
@ -1,14 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
import styles from './toggler.module.css';
|
||||
|
||||
export default ({ onChange, name, className = '', checked, label = '', plain = false }) => (
|
||||
<div className={className + ' w-fit'}>
|
||||
<label className={styles.label}>
|
||||
<div className={plain ? styles.switchPlain : styles.switch}>
|
||||
<input type={styles.checkbox} onClick={onChange} name={name} defaultChecked={checked} />
|
||||
<span className={`${plain ? styles.sliderPlain : styles.slider} ${checked ? styles.checked : ''}`} />
|
||||
</div>
|
||||
{label && <span>{label}</span>}
|
||||
</label>
|
||||
</div>
|
||||
export default ({
|
||||
onChange,
|
||||
name,
|
||||
className = '',
|
||||
checked,
|
||||
label = '',
|
||||
plain = false,
|
||||
}) => (
|
||||
<div className={className + ' w-fit'}>
|
||||
<label className={styles.label}>
|
||||
<div className={plain ? styles.switchPlain : styles.switch}>
|
||||
<input
|
||||
type={styles.checkbox}
|
||||
onClick={onChange}
|
||||
name={name}
|
||||
defaultChecked={checked}
|
||||
/>
|
||||
<span
|
||||
className={`${plain ? styles.sliderPlain : styles.slider} ${
|
||||
checked ? styles.checked : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{label && <span>{label}</span>}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,10 +22,22 @@ module.exports = {
|
|||
opacity: '1'
|
||||
// transform: 'translateY(0)'
|
||||
}
|
||||
},
|
||||
'bg-spin': {
|
||||
'0%': {
|
||||
backgroundPosition: '0 50%',
|
||||
},
|
||||
'50%': {
|
||||
backgroundPosition: '100% 50%',
|
||||
},
|
||||
'100%': {
|
||||
backgroundPosition: '0 50%',
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fade-in 0.2s ease-out'
|
||||
'fade-in': 'fade-in 0.2s ease-out',
|
||||
'bg-spin': 'bg-spin 1s ease infinite'
|
||||
},
|
||||
colors: {
|
||||
'disabled-text': 'rgba(0,0,0, 0.38)'
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue