Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 05b188c

Browse files
authored
Code Insights: Improve dashboard select UI (#43029)
* Refactor Popover UI - Add aria attribute to the popover trigger - Fix problem with return focus to target - Remove legacy RAF schedule call in tether - Add new hook API for exposing popover state * Fix Combobox UI - Fix group option layout - Fix scroll into view logic (add scroll into center functionality) * Connect forward ref and truncated text element * Make all dashboard names unique * Refactor DashboardSelect UI (use combobox + popover over the reach ui listbox) * Remove legacy dashboard select UI components * Remove @reach/listbox styles from the main style entry * Fix dashboard content import * Fix combobox popover close logic * Fix dashboard page unit tests * Update dashboard select story * Fix unit tests of UI components that use popover UI * Update feedback prompt unit tests * Fix chart tooltip return to target logic * Fix insight integration tests (dashboard select selectors) * Fix layout and dashboard option selectors * Bring back async tether scheduling * Fix jest snapshots * Remove reach ui listbox package
1 parent 2a7215e commit 05b188c

File tree

31 files changed

+601
-609
lines changed

31 files changed

+601
-609
lines changed

client/web/src/SourcegraphWebApp.scss

-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ body,
8888
// Global styles provided by @reach packages. Should be imported once in the global scope.
8989
@import '@reach/combobox/styles.css';
9090
@import '@reach/menu-button/styles.css';
91-
@import '@reach/listbox/styles.css';
9291

9392
// Pages
9493
@import './api/ApiConsole';

client/web/src/enterprise/insights/components/trancated-text/TruncatedText.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ import styles from './TruncatedText.module.scss'
99
export const TruncatedText = forwardRef((props, reference) => {
1010
const { as: Component = 'span', className, ...otherProps } = props
1111

12-
return <Component className={classNames(className, styles.truncatedText)} {...otherProps} />
12+
return <Component ref={reference} className={classNames(className, styles.truncatedText)} {...otherProps} />
1313
}) as ForwardReferenceComponent<'span'>

client/web/src/enterprise/insights/core/backend/gql-backend/methods/get-dashboards/get-dashboards.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApolloClient } from '@apollo/client'
2+
import { groupBy } from 'lodash'
23
import { Observable } from 'rxjs'
34
import { map } from 'rxjs/operators'
45

@@ -34,7 +35,7 @@ export const getDashboards = (apolloClient: ApolloClient<unknown>, id?: string):
3435

3536
return [
3637
ALL_INSIGHTS_DASHBOARD,
37-
...insightsDashboards.nodes.map(
38+
...makeDashboardTitleUnuque(insightsDashboards.nodes).map(
3839
(dashboard): InsightDashboard => ({
3940
id: dashboard.id,
4041
type: InsightsDashboardType.Custom,
@@ -46,7 +47,22 @@ export const getDashboards = (apolloClient: ApolloClient<unknown>, id?: string):
4647
})
4748
)
4849

49-
export function deserializeDashboardsOwners(
50+
function makeDashboardTitleUnuque(dashboards: InsightsDashboardNode[]): InsightsDashboardNode[] {
51+
const groupedByTitle = groupBy(dashboards, dashboard => dashboard.title)
52+
53+
return Object.keys(groupedByTitle).flatMap(title => {
54+
if (groupedByTitle[title].length === 1) {
55+
return groupedByTitle[title]
56+
}
57+
58+
return groupedByTitle[title].map((dashboard, index) => ({
59+
...dashboard,
60+
title: `${dashboard.title} (${index + 1})`,
61+
}))
62+
})
63+
}
64+
65+
function deserializeDashboardsOwners(
5066
dashboardNode: InsightsDashboardNode,
5167
userNode: InsightsDashboardCurrentUser | null
5268
): InsightsDashboardOwner[] {

client/web/src/enterprise/insights/pages/dashboards/dashboard-page/DashboardsContentPage.test.tsx

+14-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import React from 'react'
22

33
import { useApolloClient } from '@apollo/client'
44
import { MockedResponse } from '@apollo/client/testing'
5-
import { waitFor } from '@testing-library/react'
5+
import { within } from '@testing-library/dom'
6+
import { act, waitFor } from '@testing-library/react'
67
import userEvent from '@testing-library/user-event'
78
import sinon from 'sinon'
89

@@ -175,10 +176,13 @@ const triggerDashboardMenuItem = async (
175176
const dashboardMenu = await waitFor(() => screen.getByRole('img', { name: 'dashboard options' }))
176177
user.click(dashboardMenu)
177178

178-
const dashboardMenuItem = screen.getByRole('menuitem', { name: buttonText })
179+
const dialog = screen.getByRole('dialog', { hidden: true })
180+
const dashboardMenuItem = within(dialog).getByText(buttonText)
179181

180-
dashboardMenuItem.focus()
181-
user.click(dashboardMenuItem)
182+
act(() => {
183+
dashboardMenuItem.focus()
184+
user.click(dashboardMenuItem)
185+
})
182186
}
183187

184188
beforeEach(() => {
@@ -212,8 +216,12 @@ describe('DashboardsContent', () => {
212216
const chooseDashboard = await waitFor(() => screen.getByRole('button', { name: /Choose a dashboard/ }))
213217
user.click(chooseDashboard)
214218

215-
const dashboard2 = screen.getByRole('option', { name: /Global Dashboard 2/ })
216-
user.click(dashboard2)
219+
const coboboxPopover = screen.getByRole('dialog', { hidden: true })
220+
const dashboard2 = coboboxPopover.querySelector('[title="Global Dashboard 2"]')
221+
222+
if (dashboard2) {
223+
user.click(dashboard2)
224+
}
217225

218226
expect(history.location.pathname).toEqual('/insights/dashboards/bar')
219227
})

client/web/src/enterprise/insights/pages/dashboards/dashboard-page/components/dashboard-select/DashboardSelect.module.scss

+105-37
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,117 @@
1-
.list {
2-
display: block;
3-
position: static;
4-
width: 100%;
5-
overflow: auto;
6-
max-height: 32rem;
7-
// This value is synced with dashboard select input width
8-
max-width: 22.75rem;
9-
10-
&:focus {
11-
// Reach-ui implicitly reset box-shadow to none if list element is focused
12-
// to preserve visual state we have to explicitly set a box shadow value to
13-
// our standard --dropdown-shadow
14-
box-shadow: var(--dropdown-shadow);
1+
.trigger-button {
2+
display: flex;
3+
align-items: center;
4+
background-color: var(--input-bg);
5+
font-weight: normal;
6+
7+
&--text {
8+
display: flex;
9+
align-items: baseline;
10+
gap: 0.5rem;
11+
12+
/*
13+
Without min-width with value, the flex child containing the other text elements
14+
won’t narrow past the “implied width” of those text elements.
15+
16+
More info (https://css-tricks.com/flexbox-truncated-text/)
17+
*/
18+
min-width: 0;
19+
white-space: nowrap;
20+
margin-right: 0.25rem;
21+
}
22+
23+
&--icon {
24+
flex-shrink: 0;
25+
margin-left: auto;
26+
// Compensate inner view box SVG padding
27+
margin-right: -0.25rem;
28+
color: var(--icon-color);
1529
}
1630
}
1731

18-
.popover {
19-
// Reset reach-ui styles for popover element.
20-
outline: none !important;
21-
box-shadow: none !important;
22-
border: none;
23-
background: none;
32+
.badge {
33+
max-width: 8rem;
34+
flex-shrink: 0;
2435
}
2536

26-
.group-label {
27-
font-weight: normal;
28-
font-size: 0.75rem;
29-
border-top: 1px solid var(--border-color);
30-
padding: 0.5rem 0.75rem 0;
37+
.popover {
38+
display: flex;
3139
}
3240

33-
.option {
34-
margin: 0.25rem 0;
41+
.combobox {
42+
display: flex;
43+
flex-direction: column;
44+
height: 100%;
45+
max-width: 25rem;
46+
47+
&-unchanged {
48+
[data-user-value] {
49+
font-weight: normal;
50+
}
51+
}
3552

36-
& + & {
37-
margin-top: -0.25rem;
53+
&--input-container {
54+
padding: 0.5rem;
55+
position: sticky;
56+
top: 0;
57+
z-index: 1;
58+
border-bottom: 1px solid var(--border-color);
59+
background: var(--dropdown-bg);
60+
}
61+
62+
&--input {
63+
font-size: 0.75rem;
64+
padding-bottom: 0.25rem;
3865
}
66+
67+
&--list {
68+
padding-top: 0.25rem;
69+
padding-bottom: 0.25rem;
70+
@-moz-document url-prefix('') {
71+
scrollbar-width: thin;
72+
scrollbar-color: var(--text-muted);
73+
}
74+
}
75+
76+
&--option-group {
77+
[data-group-heading] {
78+
position: sticky;
79+
top: calc(-0.25rem - 1px);
80+
background-color: var(--body-bg);
81+
}
82+
}
83+
84+
&--option {
85+
display: flex;
86+
align-items: baseline;
87+
justify-content: space-between;
88+
gap: 0.5rem;
89+
padding-left: 1rem;
90+
91+
&[data-highlighted] {
92+
/* Badge-secondary variant for improved contrast when background color changes */
93+
.badge {
94+
background-color: var(--color-bg-1);
95+
border-color: var(--color-bg-1);
96+
}
97+
}
98+
99+
&-selected {
100+
background-color: var(--color-bg-3);
101+
102+
/* Badge-secondary variant for improved contrast when background color changes */
103+
.badge {
104+
background-color: var(--color-bg-1);
105+
border-color: var(--color-bg-1);
106+
}
107+
}
108+
}
109+
}
110+
111+
.no-results-found {
112+
display: flex;
113+
align-items: baseline;
114+
padding: 0.25rem 1rem;
39115
}
40116

41117
.limited-access {
@@ -57,11 +133,3 @@
57133
font-size: 0.75rem;
58134
}
59135
}
60-
61-
.limited-access-wrapper {
62-
// Override disabled visual state
63-
opacity: 1 !important;
64-
margin-bottom: 0;
65-
padding-top: 0.5rem;
66-
border-top: 1px solid var(--border-color);
67-
}

client/web/src/enterprise/insights/pages/dashboards/dashboard-page/components/dashboard-select/DashboardSelect.story.tsx

+28-12
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ const decorator: DecoratorFn = story => <WebStory>{() => story()}</WebStory>
1212
const config: Meta = {
1313
title: 'web/insights/DashboardSelect',
1414
decorators: [decorator],
15-
parameters: {
16-
chromatic: {
17-
viewports: [576, 1440],
18-
},
19-
},
2015
}
2116

2217
export default config
2318

2419
const DASHBOARDS: InsightDashboard[] = [
20+
{
21+
type: InsightsDashboardType.Virtual,
22+
id: 'ALL_INSIGHTS',
23+
title: 'All Insights',
24+
},
2525
{
2626
type: InsightsDashboardType.Custom,
2727
id: '101',
@@ -44,26 +44,42 @@ const DASHBOARDS: InsightDashboard[] = [
4444
type: InsightsDashboardType.Custom,
4545
id: '104',
4646
title: 'Sourcegraph',
47-
owners: [{ id: '104', title: 'Sourcegraph', type: InsightsDashboardOwnerType.Personal }],
47+
owners: [{ id: '104', title: 'Sourcegraph', type: InsightsDashboardOwnerType.Organization }],
4848
},
4949
{
5050
type: InsightsDashboardType.Custom,
5151
id: '105',
52-
title: 'Loooong looo0000ong name of dashboard',
53-
owners: [{ id: '104', title: 'Sourcegraph', type: InsightsDashboardOwnerType.Personal }],
52+
title: 'Loooong looo0000ong name of dashboard 1',
53+
owners: [{ id: '104', title: 'Sourcegraph', type: InsightsDashboardOwnerType.Organization }],
5454
},
5555
{
5656
type: InsightsDashboardType.Custom,
5757
id: '106',
58-
title: 'Loooong looo0000ong name of dashboard',
59-
owners: [{ id: '104', title: 'Sourcegraph', type: InsightsDashboardOwnerType.Personal }],
58+
title: 'Loooong looo0000ong name of dashboard 2',
59+
owners: [{ id: '104', title: 'Sourcegraph', type: InsightsDashboardOwnerType.Organization }],
60+
},
61+
{
62+
type: InsightsDashboardType.Custom,
63+
id: '107',
64+
title: 'Global dashboard',
65+
owners: [{ id: '101', title: 'Personal', type: InsightsDashboardOwnerType.Global }],
66+
},
67+
{
68+
type: InsightsDashboardType.Custom,
69+
id: '108',
70+
title: 'Global FE dashboard',
71+
owners: [{ id: '101', title: 'Personal', type: InsightsDashboardOwnerType.Global }],
6072
},
6173
]
6274

6375
export const DashboardSelectStory: Story = () => {
64-
const [value, setValue] = useState<string>()
76+
const [dashboard, setDashboard] = useState<InsightDashboard | undefined>()
6577

66-
return <DashboardSelect value={value} dashboards={DASHBOARDS} onSelect={dashboard => setValue(dashboard.id)} />
78+
return (
79+
<section style={{ margin: '2rem' }}>
80+
<DashboardSelect dashboard={dashboard} dashboards={DASHBOARDS} onSelect={setDashboard} />
81+
</section>
82+
)
6783
}
6884

6985
DashboardSelectStory.storyName = 'DashboardSelect'

0 commit comments

Comments
 (0)