Skip to content

Commit f68aa36

Browse files
authored
feat(draggableTabs): Add query count and tab menu button (#75114)
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
1 parent 138abac commit f68aa36

File tree

6 files changed

+270
-116
lines changed

6 files changed

+270
-116
lines changed

static/app/components/draggableTabs/draggableTab.tsx

Lines changed: 0 additions & 68 deletions
This file was deleted.

static/app/components/draggableTabs/draggableTabList.tsx

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,25 @@ import {Reorder} from 'framer-motion';
1111

1212
import type {SelectOption} from 'sentry/components/compactSelect';
1313
import {TabsContext} from 'sentry/components/tabs';
14+
import {type BaseTabProps, Tab} from 'sentry/components/tabs/tab';
1415
import {OverflowMenu, useOverflowTabs} from 'sentry/components/tabs/tabList';
1516
import {tabsShouldForwardProp} from 'sentry/components/tabs/utils';
1617
import {space} from 'sentry/styles/space';
1718
import {browserHistory} from 'sentry/utils/browserHistory';
18-
import type {Tab} from 'sentry/views/issueList/draggableTabBar';
1919

20-
import {DraggableTab} from './draggableTab';
2120
import type {DraggableTabListItemProps} from './item';
2221
import {Item} from './item';
2322

2423
interface BaseDraggableTabListProps extends DraggableTabListProps {
2524
items: DraggableTabListItemProps[];
26-
setTabs: (tabs: Tab[]) => void;
27-
tabs: Tab[];
2825
}
2926

3027
function BaseDraggableTabList({
3128
hideBorder = false,
3229
className,
3330
outerWrapStyles,
34-
tabs,
35-
setTabs,
31+
onReorder,
32+
tabVariant = 'filled',
3633
...props
3734
}: BaseDraggableTabListProps) {
3835
const tabListRef = useRef<HTMLUListElement>(null);
@@ -108,7 +105,12 @@ function BaseDraggableTabList({
108105

109106
return (
110107
<TabListOuterWrap style={outerWrapStyles}>
111-
<Reorder.Group axis="x" values={tabs} onReorder={setTabs} as="div">
108+
<Reorder.Group
109+
axis="x"
110+
values={[...state.collection]}
111+
onReorder={onReorder}
112+
as="div"
113+
>
112114
<TabListWrap
113115
{...tabListProps}
114116
orientation={orientation}
@@ -119,10 +121,10 @@ function BaseDraggableTabList({
119121
{[...state.collection].map(item => (
120122
<Reorder.Item
121123
key={item.key}
122-
value={tabs.find(tab => tab.key === item.key)}
124+
value={item}
123125
style={{display: 'flex', flexDirection: 'row'}}
124126
>
125-
<DraggableTab
127+
<Tab
126128
key={item.key}
127129
item={item}
128130
state={state}
@@ -131,7 +133,9 @@ function BaseDraggableTabList({
131133
orientation === 'horizontal' && overflowTabs.includes(item.key)
132134
}
133135
ref={element => (tabItemsRef.current[item.key] = element)}
136+
variant={tabVariant}
134137
/>
138+
135139
{state.selectedKey !== item.key &&
136140
state.collection.getKeyAfter(item.key) !== state.selectedKey && (
137141
<TabDivider />
@@ -157,23 +161,18 @@ const collectionFactory = (nodes: Iterable<Node<any>>) => new ListCollection(nod
157161
export interface DraggableTabListProps
158162
extends AriaTabListOptions<DraggableTabListItemProps>,
159163
TabListStateOptions<DraggableTabListItemProps> {
160-
setTabs: (tabs: Tab[]) => void;
161-
tabs: Tab[];
164+
onReorder: (newOrder: Node<DraggableTabListItemProps>[]) => void;
162165
className?: string;
163166
hideBorder?: boolean;
164167
outerWrapStyles?: React.CSSProperties;
168+
tabVariant?: BaseTabProps['variant'];
165169
}
166170

167171
/**
168172
* To be used as a direct child of the <Tabs /> component. See example usage
169173
* in tabs.stories.js
170174
*/
171-
export function DraggableTabList({
172-
items,
173-
tabs,
174-
setTabs,
175-
...props
176-
}: DraggableTabListProps) {
175+
export function DraggableTabList({items, ...props}: DraggableTabListProps) {
177176
const collection = useCollection({items, ...props}, collectionFactory);
178177

179178
const parsedItems = useMemo(
@@ -191,13 +190,7 @@ export function DraggableTabList({
191190
);
192191

193192
return (
194-
<BaseDraggableTabList
195-
tabs={tabs}
196-
items={parsedItems}
197-
disabledKeys={disabledKeys}
198-
setTabs={setTabs}
199-
{...props}
200-
>
193+
<BaseDraggableTabList items={parsedItems} disabledKeys={disabledKeys} {...props}>
201194
{item => <Item {...item} />}
202195
</BaseDraggableTabList>
203196
);
@@ -210,7 +203,7 @@ const TabDivider = styled('div')`
210203
width: 1px;
211204
border-radius: 6px;
212205
background-color: ${p => p.theme.gray200};
213-
margin: 9px auto;
206+
margin: 8px 4px;
214207
`;
215208

216209
const TabListOuterWrap = styled('div')`
@@ -236,7 +229,6 @@ const TabListWrap = styled('ul', {
236229
? `
237230
grid-auto-flow: column;
238231
justify-content: start;
239-
gap: ${space(0.5)};
240232
${!p.hideBorder && `border-bottom: solid 1px ${p.theme.border};`}
241233
stroke-dasharray: 4, 3;
242234
`

static/app/components/draggableTabs/index.stories.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
44
import JSXNode from 'sentry/components/stories/jsxNode';
55
import SizingWindow from 'sentry/components/stories/sizingWindow';
66
import storyBook from 'sentry/stories/storyBook';
7-
import {DraggableTabBar} from 'sentry/views/issueList/draggableTabBar';
7+
import {DraggableTabBar, type Tab} from 'sentry/views/issueList/draggableTabBar';
88

99
const TabPanelContainer = styled('div')`
1010
width: 90%;
@@ -13,21 +13,27 @@ const TabPanelContainer = styled('div')`
1313
`;
1414

1515
export default storyBook(DraggableTabBar, story => {
16-
const TABS = [
16+
const TABS: Tab[] = [
1717
{
1818
key: 'one',
1919
label: 'Inbox',
2020
content: <TabPanelContainer>This is the Inbox view</TabPanelContainer>,
21+
queryCount: 1001,
22+
hasUnsavedChanges: true,
2123
},
2224
{
2325
key: 'two',
2426
label: 'For Review',
2527
content: <TabPanelContainer>This is the For Review view</TabPanelContainer>,
28+
queryCount: 50,
29+
hasUnsavedChanges: false,
2630
},
2731
{
2832
key: 'three',
2933
label: 'Regressed',
3034
content: <TabPanelContainer>This is the Regressed view</TabPanelContainer>,
35+
queryCount: 100,
36+
hasUnsavedChanges: false,
3137
},
3238
];
3339

@@ -44,12 +50,7 @@ export default storyBook(DraggableTabBar, story => {
4450
</p>
4551
<SizingWindow>
4652
<TabBarContainer>
47-
<DraggableTabBar
48-
tabs={TABS}
49-
tempTabContent={
50-
<TabPanelContainer>This is a temporary tab</TabPanelContainer>
51-
}
52-
/>
53+
<DraggableTabBar tabs={TABS} />
5354
</TabBarContainer>
5455
</SizingWindow>
5556
</Fragment>

static/app/icons/iconEllipsis.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,23 @@ import {forwardRef} from 'react';
33
import type {SVGIconProps} from './svgIcon';
44
import {SvgIcon} from './svgIcon';
55

6-
const IconEllipsis = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
7-
return (
8-
<SvgIcon {...props} ref={ref}>
9-
<circle cx="8" cy="8" r="1.31" />
10-
<circle cx="1.31" cy="8" r="1.31" />
11-
<circle cx="14.69" cy="8" r="1.31" />
12-
</SvgIcon>
13-
);
14-
});
6+
interface IconEllipsisProps extends SVGIconProps {
7+
compact?: boolean;
8+
}
9+
10+
const IconEllipsis = forwardRef<SVGSVGElement, IconEllipsisProps>(
11+
({compact = false, ...props}: IconEllipsisProps, ref) => {
12+
const circleRadius = compact ? 1.11 : 1.31;
13+
const circleSpacing = compact ? 5.5 : 6.69;
14+
return (
15+
<SvgIcon {...props} ref={ref}>
16+
<circle cx="8" cy="8" r={circleRadius} />
17+
<circle cx={8 - circleSpacing} cy="8" r={circleRadius} />
18+
<circle cx={8 + circleSpacing} cy="8" r={circleRadius} />
19+
</SvgIcon>
20+
);
21+
}
22+
);
1523

1624
IconEllipsis.displayName = 'IconEllipsis';
1725

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,77 @@
11
import 'intersection-observer'; // polyfill
22

33
import {useState} from 'react';
4-
import type {Key} from '@react-types/shared';
4+
import styled from '@emotion/styled';
5+
import type {Key, Node} from '@react-types/shared';
56

7+
import Badge from 'sentry/components/badge/badge';
68
import {DraggableTabList} from 'sentry/components/draggableTabs/draggableTabList';
9+
import type {DraggableTabListItemProps} from 'sentry/components/draggableTabs/item';
10+
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
11+
import QueryCount from 'sentry/components/queryCount';
712
import {TabPanels, Tabs} from 'sentry/components/tabs';
13+
import {space} from 'sentry/styles/space';
14+
import {defined} from 'sentry/utils';
15+
import {DraggableTabMenuButton} from 'sentry/views/issueList/draggableTabMenuButton';
816

917
export interface Tab {
1018
content: React.ReactNode;
1119
key: Key;
1220
label: string;
21+
hasUnsavedChanges?: boolean;
1322
queryCount?: number;
1423
}
1524

1625
export interface DraggableTabBarProps {
1726
tabs: Tab[];
18-
tempTabContent: React.ReactNode;
27+
onDelete?: (key: MenuItemProps['key']) => void;
28+
onDiscard?: (key: MenuItemProps['key']) => void;
29+
onDuplicate?: (key: MenuItemProps['key']) => void;
30+
onRename?: (key: MenuItemProps['key']) => void;
31+
onSave?: (key: MenuItemProps['key']) => void;
1932
}
2033

2134
export function DraggableTabBar(props: DraggableTabBarProps) {
22-
const [tabs, setTabs] = useState<Tab[]>([...props.tabs]);
35+
const [tabs, setTabs] = useState<Tab[]>(props.tabs);
36+
const [selectedTabKey, setSelectedTabKey] = useState<Key>(props.tabs[0].key);
37+
38+
const onReorder: (newOrder: Node<DraggableTabListItemProps>[]) => void = newOrder => {
39+
setTabs(
40+
newOrder
41+
.map(node => {
42+
const foundTab = tabs.find(tab => tab.key === node.key);
43+
return foundTab?.key === node.key ? foundTab : null;
44+
})
45+
.filter(defined)
46+
);
47+
};
2348

2449
return (
2550
<Tabs>
26-
<DraggableTabList tabs={tabs} setTabs={setTabs} orientation="horizontal">
51+
<DraggableTabList
52+
onReorder={onReorder}
53+
onSelectionChange={setSelectedTabKey}
54+
orientation="horizontal"
55+
>
2756
{tabs.map(tab => (
28-
<DraggableTabList.Item key={tab.key}>{tab.label}</DraggableTabList.Item>
57+
<DraggableTabList.Item key={tab.key}>
58+
<TabContentWrap>
59+
{tab.label}
60+
<StyledBadge>
61+
<QueryCount hideParens count={tab.queryCount} max={1000} />
62+
</StyledBadge>
63+
{selectedTabKey === tab.key && (
64+
<DraggableTabMenuButton
65+
hasUnsavedChanges={tab.hasUnsavedChanges}
66+
onDelete={key => props.onDelete?.(key)}
67+
onDiscard={key => props.onDiscard?.(key)}
68+
onDuplicate={key => props.onDuplicate?.(key)}
69+
onRename={key => props.onRename?.(key)}
70+
onSave={key => props.onSave?.(key)}
71+
/>
72+
)}
73+
</TabContentWrap>
74+
</DraggableTabList.Item>
2975
))}
3076
</DraggableTabList>
3177
<TabPanels>
@@ -36,3 +82,24 @@ export function DraggableTabBar(props: DraggableTabBarProps) {
3682
</Tabs>
3783
);
3884
}
85+
86+
const TabContentWrap = styled('span')`
87+
white-space: nowrap;
88+
display: flex;
89+
align-items: center;
90+
flex-direction: row;
91+
padding: ${space(0)} ${space(0)};
92+
gap: 6px;
93+
`;
94+
95+
const StyledBadge = styled(Badge)`
96+
display: flex;
97+
height: 16px;
98+
align-items: center;
99+
justify-content: center;
100+
border-radius: 10px;
101+
background: transparent;
102+
border: 1px solid ${p => p.theme.gray200};
103+
color: ${p => p.theme.gray300};
104+
margin-left: ${space(0)};
105+
`;

0 commit comments

Comments
 (0)