mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat: improve inspector tabs (#21639)
This commit is contained in:
BIN
frontend/__snapshots__/lemon-ui-lemon-tabs--default--dark.png
Normal file
BIN
frontend/__snapshots__/lemon-ui-lemon-tabs--default--dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
frontend/__snapshots__/lemon-ui-lemon-tabs--default--light.png
Normal file
BIN
frontend/__snapshots__/lemon-ui-lemon-tabs--default--light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
frontend/__snapshots__/lemon-ui-lemon-tabs--small--dark.png
Normal file
BIN
frontend/__snapshots__/lemon-ui-lemon-tabs--small--dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
BIN
frontend/__snapshots__/lemon-ui-lemon-tabs--small--light.png
Normal file
BIN
frontend/__snapshots__/lemon-ui-lemon-tabs--small--light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
@@ -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;
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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={
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user