Skip to content

Commit

Permalink
web: add the expand/collapse buttons to the sidebar (#5639)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicks authored Apr 5, 2022
1 parent c9c96c8 commit 8676fbc
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 156 deletions.
142 changes: 95 additions & 47 deletions web/src/OverviewSidebarOptions.test.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
import { mount, ReactWrapper } from "enzyme"
import React from "react"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import React, { ReactElement } from "react"
import { MemoryRouter } from "react-router"
import { AnalyticsAction, AnalyticsType } from "./analytics"
import {
cleanupMockAnalyticsCalls,
expectIncrs,
mockAnalyticsCalls,
} from "./analytics_test_helpers"
import { accessorsForTesting, tiltfileKeyContext } from "./BrowserStorage"
import Features, { FeaturesTestProvider, Flag } from "./feature"
import {
TenResourcesWithLabels,
TestsWithErrors,
TwoResourcesTwoTests,
} from "./OverviewResourceSidebar.stories"
import {
CheckboxToggle,
OverviewSidebarOptions,
} from "./OverviewSidebarOptions"
import { OverviewSidebarOptionsRoot } from "./OverviewSidebarOptions"
import { ResourceGroupsContextProvider } from "./ResourceGroupsContext"
import {
DEFAULT_OPTIONS,
ResourceListOptions,
ResourceListOptionsProvider,
RESOURCE_LIST_OPTIONS_KEY,
} from "./ResourceListOptionsContext"
import { ResourceNameFilterTextField } from "./ResourceNameFilter"
import SidebarItemView from "./SidebarItemView"
import SidebarResources, { SidebarListSection } from "./SidebarResources"
import { StarredResourcesContextProvider } from "./StarredResourcesContext"
import { SidebarItemNameRoot, SidebarItemRoot } from "./SidebarItemView"
import {
SidebarListSectionItemsRoot,
SidebarResourcesRoot,
} from "./SidebarResources"

const resourceListOptionsAccessor = accessorsForTesting<ResourceListOptions>(
RESOURCE_LIST_OPTIONS_KEY,
Expand All @@ -37,69 +42,90 @@ const resourceListOptionsAccessor = accessorsForTesting<ResourceListOptions>(
*/

function assertSidebarItemsAndOptions(
root: ReactWrapper,
root: HTMLElement,
names: string[],
expectAlertsOnTop: boolean,
expectedResourceNameFilter?: string
) {
let sidebar = root.find(SidebarResources)
let sidebar = Array.from(root.querySelectorAll(SidebarResourcesRoot))
expect(sidebar).toHaveLength(1)

// only check items in the "all resources" section, i.e. don't look at starred things
// or we'll have duplicates
let all = sidebar.find(SidebarListSection)
let items = all.find(SidebarItemView)
const observedNames = items.map((i) => i.props().item.name)
let all = sidebar[0].querySelector(SidebarListSectionItemsRoot)!
let items = Array.from(all.querySelectorAll(SidebarItemRoot))
const observedNames = items.map(
(i) => i.querySelector(SidebarItemNameRoot)?.textContent
)
expect(observedNames).toEqual(names)

let optSetter = sidebar.find(OverviewSidebarOptions)
expect(optSetter).toHaveLength(1)
expect(optSetter.find(CheckboxToggle).prop("checked")).toEqual(
expectAlertsOnTop
let optSetter = Array.from(
sidebar[0].querySelectorAll(OverviewSidebarOptionsRoot)
)
expect(optSetter).toHaveLength(1)

let checkbox = optSetter[0].querySelector(
"input[type=checkbox]"
) as HTMLInputElement
expect(checkbox.checked).toEqual(expectAlertsOnTop)
if (expectedResourceNameFilter !== undefined) {
expect(optSetter.find(ResourceNameFilterTextField).props().value).toEqual(
expectedResourceNameFilter
)
expect(
optSetter[0].querySelector(ResourceNameFilterTextField)!.textContent
).toEqual(expectedResourceNameFilter)
}
}

describe("overview sidebar options", () => {
beforeEach(() => {
mockAnalyticsCalls()
beforeEach(() => {
mockAnalyticsCalls()
})

afterEach(() => {
cleanupMockAnalyticsCalls()
localStorage.clear()
resourceListOptionsAccessor.set({
...DEFAULT_OPTIONS,
})
})

afterEach(() => {
cleanupMockAnalyticsCalls()
localStorage.clear()
function renderContainer(x: ReactElement) {
const features = new Features({
[Flag.Labels]: true,
[Flag.DisableResources]: true,
})
const { container } = render(
<MemoryRouter>
<FeaturesTestProvider value={features}>
<tiltfileKeyContext.Provider value="test">
<ResourceGroupsContextProvider>
<ResourceListOptionsProvider>{x}</ResourceListOptionsProvider>
</ResourceGroupsContextProvider>
</tiltfileKeyContext.Provider>
</FeaturesTestProvider>
</MemoryRouter>
)
return container
}

describe("overview sidebar options", () => {
it("says no matches found", () => {
resourceListOptionsAccessor.set({
...DEFAULT_OPTIONS,
resourceNameFilter: "asdfawfwef",
})
const root = mount(
<MemoryRouter>
<tiltfileKeyContext.Provider value="test">
<ResourceListOptionsProvider>
<StarredResourcesContextProvider>
{TwoResourcesTwoTests()}
</StarredResourcesContextProvider>
</ResourceListOptionsProvider>
</tiltfileKeyContext.Provider>
</MemoryRouter>
const container = renderContainer(<TwoResourcesTwoTests />)
const resourceSectionItems = Array.from(
container
.querySelector(SidebarListSectionItemsRoot)!
.querySelectorAll("li")
)

const resourceSectionItems = root.find(SidebarListSection).find("li")
expect(resourceSectionItems.map((n) => n.text())).toEqual([
expect(resourceSectionItems.map((n) => n.textContent)).toEqual([
"No matching resources",
])
})
})

it("toggles/untoggles Alerts On Top sorting when button clicked", () => {
const root = mount(TestsWithErrors())
const { container } = render(TestsWithErrors())

const origOrder = [
"(Tiltfile)",
Expand All @@ -123,13 +149,35 @@ it("toggles/untoggles Alerts On Top sorting when button clicked", () => {
"test_5",
"test_7",
]
assertSidebarItemsAndOptions(root, origOrder, false)
assertSidebarItemsAndOptions(container, origOrder, false)

let aotToggle = screen.getByLabelText("Alerts on top")
userEvent.click(aotToggle)

let aotToggle = root.find(CheckboxToggle)
aotToggle.simulate("click")
assertSidebarItemsAndOptions(container, alertsOnTopOrder, true)

assertSidebarItemsAndOptions(root, alertsOnTopOrder, true)
userEvent.click(aotToggle)
assertSidebarItemsAndOptions(container, origOrder, false)
})

aotToggle.simulate("click")
assertSidebarItemsAndOptions(root, origOrder, false)
describe("expand-all button", () => {
it("sends analytics onclick", () => {
const container = renderContainer(<TenResourcesWithLabels />)
userEvent.click(screen.getByTitle("Expand All"))
expectIncrs({
name: "ui.web.expandAllGroups",
tags: { action: AnalyticsAction.Click, type: AnalyticsType.Detail },
})
})
})

describe("collapse-all button", () => {
it("sends analytics onclick", () => {
const container = renderContainer(<TenResourcesWithLabels />)
userEvent.click(screen.getByTitle("Collapse All"))
expectIncrs({
name: "ui.web.collapseAllGroups",
tags: { action: AnalyticsAction.Click, type: AnalyticsType.Detail },
})
})
})
108 changes: 91 additions & 17 deletions web/src/OverviewSidebarOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { FormControlLabel } from "@material-ui/core"
import React from "react"
import React, { useCallback, useMemo } from "react"
import styled from "styled-components"
import { AnalyticsType } from "./analytics"
import { Flag, useFeatures } from "./feature"
import { InstrumentedCheckbox } from "./instrumentedComponents"
import { TILTFILE_LABEL, UNLABELED_LABEL } from "./labels"
import { CollapseButton, ExpandButton } from "./resourceListOptionsButtons"
import { useResourceListOptions } from "./ResourceListOptionsContext"
import { ResourceNameFilter } from "./ResourceNameFilter"
import SidebarItem from "./SidebarItem"
import { sidebarItemIsDisabled } from "./SidebarItemView"
import {
Color,
Font,
FontSize,
mixinResetListStyle,
SizeUnit,
} from "./style-helpers"
import { ResourceName } from "./types"

const OverviewSidebarOptionsRoot = styled.div`
export const OverviewSidebarOptionsRoot = styled.div`
display: flex;
justify-content: space-between;
font-family: ${Font.monospace};
Expand All @@ -25,11 +30,11 @@ const OverviewSidebarOptionsRoot = styled.div`
flex-direction: column;
`

const OverviewSidebarOptionsButtonsRoot = styled.div`
const OverviewSidebarOptionsButtonRow = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: flex-start;
justify-content: space-between;
width: 100%;
`

Expand All @@ -53,24 +58,82 @@ export const CheckboxToggle = styled(InstrumentedCheckbox)`
}
`

export function OverviewSidebarOptions() {
const { options, setOptions } = useResourceListOptions()
// Create a list of all the groups from the list of resources.
//
// Sadly, this logic is duplicated several times across table and sidebar,
// but there's no easy way to consolidate it right now.
function toGroups(
items: SidebarItem[],
hideDisabledResources: boolean
): string[] {
let hasUnlabeled = false
let hasTiltfile = false
let hasLabels: { [key: string]: boolean } = {}
items.forEach((item) => {
const isDisabled = sidebarItemIsDisabled(item)
if (hideDisabledResources && isDisabled) {
return
}

const labels = item.labels
const isTiltfile = item.name === ResourceName.tiltfile
if (labels.length) {
labels.forEach((label) => {
hasLabels[label] = true
})
} else if (isTiltfile) {
hasTiltfile = true
} else {
hasUnlabeled = true
}
})

let groups = Object.keys(hasLabels)
if (groups.length) {
if (hasTiltfile) {
groups.push(TILTFILE_LABEL)
}
if (hasUnlabeled) {
groups.push(UNLABELED_LABEL)
}
}
return groups
}

export function OverviewSidebarOptions(props: { items?: SidebarItem[] }) {
const features = useFeatures()
const { options, setOptions } = useResourceListOptions()
let toggleDisabledResources = useCallback(() => {
setOptions({
showDisabledResources: !options.showDisabledResources,
})
}, [options.showDisabledResources])

const disableResourcesEnabled = features.isEnabled(Flag.DisableResources)
const disabledResourcesToggle = disableResourcesEnabled ? (
const labelsEnabled = features.isEnabled(Flag.Labels)
let items = props.items || []

const hideDisabledResources =
!features.isEnabled(Flag.DisableResources) || !options.showDisabledResources

// TODO(nick): Enable/disable the expand/collapse button based
// on whether the groups are shown and the current group state.
let groups = useMemo(
() => toGroups(items, hideDisabledResources),
[items, hideDisabledResources]
)
const resourceFilterApplied = options.resourceNameFilter.length > 0
const displayResourceGroups =
labelsEnabled && groups.length && !resourceFilterApplied

const disabledResourcesToggle = features.isEnabled(Flag.DisableResources) ? (
<SidebarOptionsLabel
control={
<CheckboxToggle
analyticsName="ui.web.disabledResourcesToggle"
analyticsTags={{ type: AnalyticsType.Detail }}
size="small"
checked={options.showDisabledResources}
onClick={(_e) =>
setOptions({
showDisabledResources: !options.showDisabledResources,
})
}
onClick={toggleDisabledResources}
/>
}
label="Show disabled resources"
Expand All @@ -79,8 +142,8 @@ export function OverviewSidebarOptions() {

return (
<OverviewSidebarOptionsRoot>
<OverviewSidebarOptionsButtonsRoot>
{disabledResourcesToggle}
<ResourceNameFilter />
<OverviewSidebarOptionsButtonRow>
<SidebarOptionsLabel
control={
<CheckboxToggle
Expand All @@ -94,8 +157,19 @@ export function OverviewSidebarOptions() {
}
label="Alerts on top"
/>
</OverviewSidebarOptionsButtonsRoot>
<ResourceNameFilter />
<div>
<ExpandButton
disabled={!displayResourceGroups}
analyticsType={AnalyticsType.Detail}
/>
<CollapseButton
groups={groups}
disabled={!displayResourceGroups}
analyticsType={AnalyticsType.Detail}
/>
</div>
</OverviewSidebarOptionsButtonRow>
{disabledResourcesToggle}
</OverviewSidebarOptionsRoot>
)
}
Loading

0 comments on commit 8676fbc

Please sign in to comment.