Skip to content

Commit

Permalink
feat(draggableTabs): Add query count and tab menu button (#75114)
Browse files Browse the repository at this point in the history
closes #73224

This PR adds the query count and tab dropdown and button to the
draggable tabs component. These changes are purely visual, no
functionality for the dropdown menu options have been implemented.

<img width="460" alt="image"
src="https://github.com/user-attachments/assets/b22ff7d8-be53-432d-8232-a12f5db7709e">

As an added bonus, I also have cleaned up the DraggableTabList and
DraggableTab components so that they are a lot more generic than the
previous implementation of this feature

Known Issues:
- Dragging a tab to the left makes it disappear into the overflow menu
for some reason
- Hovering over the elements in the dropdown menu causes the tab itself
to increase in opacity as if its being hovered over. Clicking on an
option in the dropdown menu also increases the opacity of the tab
  • Loading branch information
MichaelSun48 authored Jul 29, 2024
1 parent 138abac commit f68aa36
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 116 deletions.
68 changes: 0 additions & 68 deletions static/app/components/draggableTabs/draggableTab.tsx

This file was deleted.

44 changes: 18 additions & 26 deletions static/app/components/draggableTabs/draggableTabList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,25 @@ import {Reorder} from 'framer-motion';

import type {SelectOption} from 'sentry/components/compactSelect';
import {TabsContext} from 'sentry/components/tabs';
import {type BaseTabProps, Tab} from 'sentry/components/tabs/tab';
import {OverflowMenu, useOverflowTabs} from 'sentry/components/tabs/tabList';
import {tabsShouldForwardProp} from 'sentry/components/tabs/utils';
import {space} from 'sentry/styles/space';
import {browserHistory} from 'sentry/utils/browserHistory';
import type {Tab} from 'sentry/views/issueList/draggableTabBar';

import {DraggableTab} from './draggableTab';
import type {DraggableTabListItemProps} from './item';
import {Item} from './item';

interface BaseDraggableTabListProps extends DraggableTabListProps {
items: DraggableTabListItemProps[];
setTabs: (tabs: Tab[]) => void;
tabs: Tab[];
}

function BaseDraggableTabList({
hideBorder = false,
className,
outerWrapStyles,
tabs,
setTabs,
onReorder,
tabVariant = 'filled',
...props
}: BaseDraggableTabListProps) {
const tabListRef = useRef<HTMLUListElement>(null);
Expand Down Expand Up @@ -108,7 +105,12 @@ function BaseDraggableTabList({

return (
<TabListOuterWrap style={outerWrapStyles}>
<Reorder.Group axis="x" values={tabs} onReorder={setTabs} as="div">
<Reorder.Group
axis="x"
values={[...state.collection]}
onReorder={onReorder}
as="div"
>
<TabListWrap
{...tabListProps}
orientation={orientation}
Expand All @@ -119,10 +121,10 @@ function BaseDraggableTabList({
{[...state.collection].map(item => (
<Reorder.Item
key={item.key}
value={tabs.find(tab => tab.key === item.key)}
value={item}
style={{display: 'flex', flexDirection: 'row'}}
>
<DraggableTab
<Tab
key={item.key}
item={item}
state={state}
Expand All @@ -131,7 +133,9 @@ function BaseDraggableTabList({
orientation === 'horizontal' && overflowTabs.includes(item.key)
}
ref={element => (tabItemsRef.current[item.key] = element)}
variant={tabVariant}
/>

{state.selectedKey !== item.key &&
state.collection.getKeyAfter(item.key) !== state.selectedKey && (
<TabDivider />
Expand All @@ -157,23 +161,18 @@ const collectionFactory = (nodes: Iterable<Node<any>>) => new ListCollection(nod
export interface DraggableTabListProps
extends AriaTabListOptions<DraggableTabListItemProps>,
TabListStateOptions<DraggableTabListItemProps> {
setTabs: (tabs: Tab[]) => void;
tabs: Tab[];
onReorder: (newOrder: Node<DraggableTabListItemProps>[]) => void;
className?: string;
hideBorder?: boolean;
outerWrapStyles?: React.CSSProperties;
tabVariant?: BaseTabProps['variant'];
}

/**
* To be used as a direct child of the <Tabs /> component. See example usage
* in tabs.stories.js
*/
export function DraggableTabList({
items,
tabs,
setTabs,
...props
}: DraggableTabListProps) {
export function DraggableTabList({items, ...props}: DraggableTabListProps) {
const collection = useCollection({items, ...props}, collectionFactory);

const parsedItems = useMemo(
Expand All @@ -191,13 +190,7 @@ export function DraggableTabList({
);

return (
<BaseDraggableTabList
tabs={tabs}
items={parsedItems}
disabledKeys={disabledKeys}
setTabs={setTabs}
{...props}
>
<BaseDraggableTabList items={parsedItems} disabledKeys={disabledKeys} {...props}>
{item => <Item {...item} />}
</BaseDraggableTabList>
);
Expand All @@ -210,7 +203,7 @@ const TabDivider = styled('div')`
width: 1px;
border-radius: 6px;
background-color: ${p => p.theme.gray200};
margin: 9px auto;
margin: 8px 4px;
`;

const TabListOuterWrap = styled('div')`
Expand All @@ -236,7 +229,6 @@ const TabListWrap = styled('ul', {
? `
grid-auto-flow: column;
justify-content: start;
gap: ${space(0.5)};
${!p.hideBorder && `border-bottom: solid 1px ${p.theme.border};`}
stroke-dasharray: 4, 3;
`
Expand Down
17 changes: 9 additions & 8 deletions static/app/components/draggableTabs/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import JSXNode from 'sentry/components/stories/jsxNode';
import SizingWindow from 'sentry/components/stories/sizingWindow';
import storyBook from 'sentry/stories/storyBook';
import {DraggableTabBar} from 'sentry/views/issueList/draggableTabBar';
import {DraggableTabBar, type Tab} from 'sentry/views/issueList/draggableTabBar';

const TabPanelContainer = styled('div')`
width: 90%;
Expand All @@ -13,21 +13,27 @@ const TabPanelContainer = styled('div')`
`;

export default storyBook(DraggableTabBar, story => {
const TABS = [
const TABS: Tab[] = [
{
key: 'one',
label: 'Inbox',
content: <TabPanelContainer>This is the Inbox view</TabPanelContainer>,
queryCount: 1001,
hasUnsavedChanges: true,
},
{
key: 'two',
label: 'For Review',
content: <TabPanelContainer>This is the For Review view</TabPanelContainer>,
queryCount: 50,
hasUnsavedChanges: false,
},
{
key: 'three',
label: 'Regressed',
content: <TabPanelContainer>This is the Regressed view</TabPanelContainer>,
queryCount: 100,
hasUnsavedChanges: false,
},
];

Expand All @@ -44,12 +50,7 @@ export default storyBook(DraggableTabBar, story => {
</p>
<SizingWindow>
<TabBarContainer>
<DraggableTabBar
tabs={TABS}
tempTabContent={
<TabPanelContainer>This is a temporary tab</TabPanelContainer>
}
/>
<DraggableTabBar tabs={TABS} />
</TabBarContainer>
</SizingWindow>
</Fragment>
Expand Down
26 changes: 17 additions & 9 deletions static/app/icons/iconEllipsis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ import {forwardRef} from 'react';
import type {SVGIconProps} from './svgIcon';
import {SvgIcon} from './svgIcon';

const IconEllipsis = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
return (
<SvgIcon {...props} ref={ref}>
<circle cx="8" cy="8" r="1.31" />
<circle cx="1.31" cy="8" r="1.31" />
<circle cx="14.69" cy="8" r="1.31" />
</SvgIcon>
);
});
interface IconEllipsisProps extends SVGIconProps {
compact?: boolean;
}

const IconEllipsis = forwardRef<SVGSVGElement, IconEllipsisProps>(
({compact = false, ...props}: IconEllipsisProps, ref) => {
const circleRadius = compact ? 1.11 : 1.31;
const circleSpacing = compact ? 5.5 : 6.69;
return (
<SvgIcon {...props} ref={ref}>
<circle cx="8" cy="8" r={circleRadius} />
<circle cx={8 - circleSpacing} cy="8" r={circleRadius} />
<circle cx={8 + circleSpacing} cy="8" r={circleRadius} />
</SvgIcon>
);
}
);

IconEllipsis.displayName = 'IconEllipsis';

Expand Down
77 changes: 72 additions & 5 deletions static/app/views/issueList/draggableTabBar.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,77 @@
import 'intersection-observer'; // polyfill

import {useState} from 'react';
import type {Key} from '@react-types/shared';
import styled from '@emotion/styled';
import type {Key, Node} from '@react-types/shared';

import Badge from 'sentry/components/badge/badge';
import {DraggableTabList} from 'sentry/components/draggableTabs/draggableTabList';
import type {DraggableTabListItemProps} from 'sentry/components/draggableTabs/item';
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
import QueryCount from 'sentry/components/queryCount';
import {TabPanels, Tabs} from 'sentry/components/tabs';
import {space} from 'sentry/styles/space';
import {defined} from 'sentry/utils';
import {DraggableTabMenuButton} from 'sentry/views/issueList/draggableTabMenuButton';

export interface Tab {
content: React.ReactNode;
key: Key;
label: string;
hasUnsavedChanges?: boolean;
queryCount?: number;
}

export interface DraggableTabBarProps {
tabs: Tab[];
tempTabContent: React.ReactNode;
onDelete?: (key: MenuItemProps['key']) => void;
onDiscard?: (key: MenuItemProps['key']) => void;
onDuplicate?: (key: MenuItemProps['key']) => void;
onRename?: (key: MenuItemProps['key']) => void;
onSave?: (key: MenuItemProps['key']) => void;
}

export function DraggableTabBar(props: DraggableTabBarProps) {
const [tabs, setTabs] = useState<Tab[]>([...props.tabs]);
const [tabs, setTabs] = useState<Tab[]>(props.tabs);
const [selectedTabKey, setSelectedTabKey] = useState<Key>(props.tabs[0].key);

const onReorder: (newOrder: Node<DraggableTabListItemProps>[]) => void = newOrder => {
setTabs(
newOrder
.map(node => {
const foundTab = tabs.find(tab => tab.key === node.key);
return foundTab?.key === node.key ? foundTab : null;
})
.filter(defined)
);
};

return (
<Tabs>
<DraggableTabList tabs={tabs} setTabs={setTabs} orientation="horizontal">
<DraggableTabList
onReorder={onReorder}
onSelectionChange={setSelectedTabKey}
orientation="horizontal"
>
{tabs.map(tab => (
<DraggableTabList.Item key={tab.key}>{tab.label}</DraggableTabList.Item>
<DraggableTabList.Item key={tab.key}>
<TabContentWrap>
{tab.label}
<StyledBadge>
<QueryCount hideParens count={tab.queryCount} max={1000} />
</StyledBadge>
{selectedTabKey === tab.key && (
<DraggableTabMenuButton
hasUnsavedChanges={tab.hasUnsavedChanges}
onDelete={key => props.onDelete?.(key)}
onDiscard={key => props.onDiscard?.(key)}
onDuplicate={key => props.onDuplicate?.(key)}
onRename={key => props.onRename?.(key)}
onSave={key => props.onSave?.(key)}
/>
)}
</TabContentWrap>
</DraggableTabList.Item>
))}
</DraggableTabList>
<TabPanels>
Expand All @@ -36,3 +82,24 @@ export function DraggableTabBar(props: DraggableTabBarProps) {
</Tabs>
);
}

const TabContentWrap = styled('span')`
white-space: nowrap;
display: flex;
align-items: center;
flex-direction: row;
padding: ${space(0)} ${space(0)};
gap: 6px;
`;

const StyledBadge = styled(Badge)`
display: flex;
height: 16px;
align-items: center;
justify-content: center;
border-radius: 10px;
background: transparent;
border: 1px solid ${p => p.theme.gray200};
color: ${p => p.theme.gray300};
margin-left: ${space(0)};
`;
Loading

0 comments on commit f68aa36

Please sign in to comment.