feat: improve inspector tabs (#21639)

This commit is contained in:
David Newell
2024-04-19 10:28:28 +01:00
committed by GitHub
parent b24c614430
commit 50df39c95b
8 changed files with 170 additions and 148 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -1,6 +1,6 @@
.LemonTabs {
--lemon-tabs-margin-bottom: 1rem;
--lemon-tabs-gap: 2rem;
--lemon-tabs-margin-bottom: 1rem;
--lemon-tabs-content-padding: 0.75rem 0;
position: relative;
@@ -8,6 +8,12 @@
flex-direction: column;
align-self: stretch;
&--small {
--lemon-tabs-gap: 1rem;
--lemon-tabs-margin-bottom: 0.5rem;
--lemon-tabs-content-padding: 0.375rem 0;
}
.Navigation3000__scene > &:first-child,
.Navigation3000__scene > :first-child > &:first-child {
margin-top: -0.75rem;

View File

@@ -50,5 +50,8 @@ const Template: StoryFn<typeof LemonTabsComponent> = (props) => {
return <LemonTabsComponent {...props} activeKey={activeKey} onChange={(newValue) => setActiveKey(newValue)} />
}
export const LemonTabs: Story = Template.bind({})
LemonTabs.args = {}
export const Default: Story = Template.bind({})
Default.args = {}
export const Small: Story = Template.bind({})
Small.args = { size: 'small' }

View File

@@ -28,6 +28,7 @@ export interface LemonTabsProps<T extends string | number> {
onChange?: (key: T) => void
/** List of tabs. Falsy entries are ignored - they're there to make conditional tabs convenient. */
tabs: (LemonTab<T> | null | false)[]
size?: 'small' | 'medium'
'data-attr'?: string
}
@@ -40,6 +41,7 @@ export function LemonTabs<T extends string | number>({
activeKey,
onChange,
tabs,
size = 'medium',
'data-attr': dataAttr,
}: LemonTabsProps<T>): JSX.Element {
const { containerRef, selectionRef, sliderWidth, sliderOffset, transitioning } = useSliderPositioning<
@@ -53,7 +55,7 @@ export function LemonTabs<T extends string | number>({
return (
<div
className={clsx('LemonTabs', transitioning && 'LemonTabs--transitioning')}
className={clsx('LemonTabs', transitioning && 'LemonTabs--transitioning', `LemonTabs--${size}`)}
// eslint-disable-next-line react/forbid-dom-props
style={
{

View File

@@ -1,5 +1,5 @@
import { IconBug, IconClock, IconDashboard, IconInfo, IconPause, IconTerminal, IconX } from '@posthog/icons'
import { LemonButton, LemonCheckbox, LemonInput, LemonSelect, Tooltip } from '@posthog/lemon-ui'
import { LemonButton, LemonCheckbox, LemonInput, LemonSelect, LemonTabs, Tooltip } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { FEATURE_FLAGS } from 'lib/constants'
import { IconPlayCircle, IconUnverifiedEvent } from 'lib/lemon-ui/icons'
@@ -39,25 +39,30 @@ function TabButtons({
const { setTab } = useActions(inspectorLogic)
return (
<>
{tabs.map((tabId) => {
<LemonTabs
size="small"
activeKey={tab}
onChange={(tabId) => setTab(tabId)}
tabs={tabs.map((tabId) => {
const TabIcon = TabToIcon[tabId]
return (
<LemonButton
key={tabId}
size="small"
// We want to indicate the tab is loading, but not disable it so we just override the icon here
icon={
TabIcon ? tabsState[tabId] === 'loading' ? <Spinner textColored /> : <TabIcon /> : undefined
}
active={tab === tabId}
onClick={() => setTab(tabId)}
>
{capitalizeFirstLetter(tabId)}
</LemonButton>
)
return {
key: tabId,
label: (
<div className="flex items-center gap-1">
{TabIcon ? (
tabsState[tabId] === 'loading' ? (
<Spinner textColored />
) : (
<TabIcon />
)
) : undefined}
<span>{capitalizeFirstLetter(tabId)}</span>
</div>
),
}
})}
</>
/>
)
}
@@ -97,148 +102,154 @@ export function PlayerInspectorControls({ onClose }: { onClose: () => void }): J
}
return (
<div className="bg-side p-2 space-y-2 border-b">
<div className="flex justify-between gap-2 flex-nowrap">
<div className="flex flex-1 items-center gap-1">
<TabButtons tabs={tabs} logicProps={logicProps} />
<div className="bg-side border-b">
<div className="flex justify-between flex-nowrap">
<div className="w-2.5 mb-2 border-b shrink-0" />
<TabButtons tabs={tabs} logicProps={logicProps} />
<div className="flex flex-1 items-center justify-end mb-2 border-b px-1">
<LemonButton size="xsmall" icon={<IconX />} onClick={onClose} />
</div>
<LemonButton size="small" icon={<IconX />} onClick={onClose} />
</div>
<div className="flex items-center gap-1 flex-wrap font-medium text-primary-alt" data-attr="mini-filters">
{miniFilters.map((filter) => (
<LemonButton
key={filter.key}
size="small"
noPadding
active={filter.enabled}
onClick={() => {
// "alone" should always be a select-to-true action
setMiniFilter(filter.key, filter.alone || !filter.enabled)
}}
tooltip={filter.tooltip}
>
<span className="p-1 text-xs">{filter.name}</span>
</LemonButton>
))}
</div>
<div className="flex items-center gap-8 justify-between">
<div className="flex items-center gap-2 flex-1">
<div className="flex flex-1">
<LemonInput
<div className="px-2">
<div
className="flex items-center gap-1 flex-wrap font-medium text-primary-alt"
data-attr="mini-filters"
>
{miniFilters.map((filter) => (
<LemonButton
key={filter.key}
size="small"
onChange={(e) => setSearchQuery(e)}
placeholder="Search..."
type="search"
value={searchQuery}
fullWidth
suffix={
<Tooltip title={<InspectorSearchInfo />}>
noPadding
active={filter.enabled}
onClick={() => {
// "alone" should always be a select-to-true action
setMiniFilter(filter.key, filter.alone || !filter.enabled)
}}
tooltip={filter.tooltip}
>
<span className="p-1 text-xs">{filter.name}</span>
</LemonButton>
))}
</div>
<div className="flex items-center py-1 gap-8 justify-between">
<div className="flex items-center gap-2 flex-1">
<div className="flex flex-1">
<LemonInput
size="small"
onChange={(e) => setSearchQuery(e)}
placeholder="Search..."
type="search"
value={searchQuery}
fullWidth
suffix={
<Tooltip title={<InspectorSearchInfo />}>
<IconInfo />
</Tooltip>
}
/>
</div>
{windowIds.length > 1 ? (
<div className="flex items-center gap-2 flex-wrap">
<LemonSelect
size="small"
data-attr="player-window-select"
value={windowIdFilter}
onChange={(val) => setWindowIdFilter(val || null)}
options={[
{
value: null,
label: 'All windows',
icon: <IconWindow size="small" value="A" className="text-muted" />,
},
...windowIds.map((windowId, index) => ({
value: windowId,
label: `Window ${index + 1}`,
icon: <IconWindow size="small" value={index + 1} className="text-muted" />,
})),
]}
/>
<Tooltip
title="Each recording window translates to a distinct browser tab or window."
className="text-base text-muted-alt"
>
<IconInfo />
</Tooltip>
}
/>
</div>
) : null}
</div>
{windowIds.length > 1 ? (
<div className="flex items-center gap-2 flex-wrap">
<LemonSelect
size="small"
data-attr="player-window-select"
value={windowIdFilter}
onChange={(val) => setWindowIdFilter(val || null)}
options={[
{
value: null,
label: 'All windows',
icon: <IconWindow size="small" value="A" className="text-muted" />,
},
...windowIds.map((windowId, index) => ({
value: windowId,
label: `Window ${index + 1}`,
icon: <IconWindow size="small" value={index + 1} className="text-muted" />,
})),
]}
/>
<div className="flex items-center gap-1">
<LemonButton
size="small"
type="secondary"
noPadding
onClick={() => setTimestampMode(timestampMode === 'absolute' ? 'relative' : 'absolute')}
tooltipPlacement="left"
tooltip={
timestampMode === 'absolute'
? 'Showing absolute timestamps'
: 'Showing timestamps relative to the start of the recording'
}
>
<span className="p-1 flex items-center gap-1">
<span className=" text-xs">{capitalizeFirstLetter(timestampMode)}</span>{' '}
<IconClock className="text-lg" />
</span>
</LemonButton>
<LemonButton
size="small"
type="secondary"
noPadding
active={syncScroll}
onClick={() => {
// If the user has syncScrolling on, but it is paused due to interacting with the Inspector, we want to resume it
if (syncScroll && syncScrollingPaused) {
setSyncScrollPaused(false)
} else {
// Otherwise we are just toggling the setting
setSyncScroll(!syncScroll)
}
}}
tooltipPlacement="left"
tooltip={
syncScroll && syncScrollingPaused
? 'Synced scrolling is paused - click to resume'
: 'Scroll the list in sync with the recording playback'
}
>
{syncScroll && syncScrollingPaused ? (
<IconPause className="text-lg m-1" />
) : (
<IconPlayCircle className="text-lg m-1" />
)}
</LemonButton>
</div>
</div>
{showMatchingEventsFilter ? (
<div className="flex items-center">
<span className="flex items-center whitespace-nowrap text-xs gap-1">
Only events matching filters
<Tooltip
title="Each recording window translates to a distinct browser tab or window."
title="Display only the events that match the global filter."
className="text-base text-muted-alt"
>
<IconInfo />
</Tooltip>
</div>
) : null}
</div>
<div className="flex items-center gap-1">
<LemonButton
size="small"
type="secondary"
noPadding
onClick={() => setTimestampMode(timestampMode === 'absolute' ? 'relative' : 'absolute')}
tooltipPlacement="left"
tooltip={
timestampMode === 'absolute'
? 'Showing absolute timestamps'
: 'Showing timestamps relative to the start of the recording'
}
>
<span className="p-1 flex items-center gap-1">
<span className=" text-xs">{capitalizeFirstLetter(timestampMode)}</span>{' '}
<IconClock className="text-lg" />
</span>
</LemonButton>
<LemonButton
size="small"
type="secondary"
noPadding
active={syncScroll}
onClick={() => {
// If the user has syncScrolling on, but it is paused due to interacting with the Inspector, we want to resume it
if (syncScroll && syncScrollingPaused) {
setSyncScrollPaused(false)
} else {
// Otherwise we are just toggling the setting
setSyncScroll(!syncScroll)
}
}}
tooltipPlacement="left"
tooltip={
syncScroll && syncScrollingPaused
? 'Synced scrolling is paused - click to resume'
: 'Scroll the list in sync with the recording playback'
}
>
{syncScroll && syncScrollingPaused ? (
<IconPause className="text-lg m-1" />
) : (
<IconPlayCircle className="text-lg m-1" />
)}
</LemonButton>
</div>
<LemonCheckbox
className="mx-2"
checked={showOnlyMatching}
size="small"
onChange={setShowOnlyMatching}
/>
</div>
) : null}
</div>
{showMatchingEventsFilter ? (
<div className="flex items-center">
<span className="flex items-center whitespace-nowrap text-xs gap-1">
Only events matching filters
<Tooltip
title="Display only the events that match the global filter."
className="text-base text-muted-alt"
>
<IconInfo />
</Tooltip>
</span>
<LemonCheckbox
className="mx-2"
checked={showOnlyMatching}
size="small"
onChange={setShowOnlyMatching}
/>
</div>
) : null}
</div>
)
}