Skip to content

Commit

Permalink
web: persist sidebar options in localStorage (#4157)
Browse files Browse the repository at this point in the history
  • Loading branch information
landism authored Feb 8, 2021
1 parent 25a1ae7 commit 195b562
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 201 deletions.
36 changes: 35 additions & 1 deletion web/src/LocalStorage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from "react"
import React, { Dispatch, SetStateAction, useContext } from "react"
import { useStorageState } from "react-storage-hooks"

export type TiltfileKey = string
Expand All @@ -8,6 +8,31 @@ export function makeKey(tiltfileKey: string, key: string): string {
return "tilt-" + JSON.stringify({ tiltfileKey: tiltfileKey, key: key })
}

export type accessor<S> = {
get: () => S | null
set: (s: S) => void
}

export function accessorsForTesting<S>(name: string) {
const key = makeKey("test", name)
function get(): S | null {
const v = localStorage.getItem(key)
if (!v) {
return null
}
return JSON.parse(v) as S
}

function set(s: S): void {
localStorage.setItem(key, JSON.stringify(s))
}

return {
get: get,
set: set,
}
}

// Like `useState`, but backed by localStorage and namespaced by the tiltfileKey
export function usePersistentState<S>(name: string, defaultValue: S) {
const tiltfileKey = useContext(tiltfileKeyContext)
Expand All @@ -17,3 +42,12 @@ export function usePersistentState<S>(name: string, defaultValue: S) {
defaultValue
)
}

export function PersistentStateProvider<S>(props: {
name: string
defaultValue: S
children: (state: S, setState: Dispatch<SetStateAction<S>>) => JSX.Element
}) {
const [state, setState] = usePersistentState(props.name, props.defaultValue)
return props.children(state, setState)
}
11 changes: 6 additions & 5 deletions web/src/OverviewPane.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mount, ReactWrapper } from "enzyme"
import React from "react"
import { MemoryRouter } from "react-router"
import { makeKey, tiltfileKeyContext } from "./LocalStorage"
import { accessorsForTesting, tiltfileKeyContext } from "./LocalStorage"
import OverviewItemView from "./OverviewItemView"
import OverviewPane, {
AllResources,
Expand All @@ -27,6 +27,10 @@ function assertContainerWithResources(
}
}

const pinnedResourcesAccessor = accessorsForTesting<string[]>(
"pinned-resources"
)

it("renders all resources if no pinned and no tests", () => {
const root = mount(
<MemoryRouter initialEntries={["/"]}>{TwoResources()}</MemoryRouter>
Expand All @@ -38,10 +42,7 @@ it("renders all resources if no pinned and no tests", () => {
})

it("renders pinned resources", () => {
localStorage.setItem(
makeKey("test", "pinned-resources"),
JSON.stringify(["snack"])
)
pinnedResourcesAccessor.set(["snack"])

const root = mount(
<MemoryRouter initialEntries={["/"]}>
Expand Down
257 changes: 137 additions & 120 deletions web/src/OverviewSidebarOptions.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { mount, ReactWrapper } from "enzyme"
import React from "react"
import { MemoryRouter } from "react-router"
import { makeKey, tiltfileKeyContext } from "./LocalStorage"
import { accessorsForTesting, tiltfileKeyContext } from "./LocalStorage"
import { TwoResourcesTwoTests } from "./OverviewResourceSidebar.stories"
import { OverviewSidebarOptions } from "./OverviewSidebarOptions"
import {
AlertsOnTopToggle,
OverviewSidebarOptions,
} from "./OverviewSidebarOptions"
import PathBuilder from "./PathBuilder"
import SidebarItem from "./SidebarItem"
import SidebarItemView from "./SidebarItemView"
Expand All @@ -14,11 +17,16 @@ import { ResourceView } from "./types"

let pathBuilder = PathBuilder.forTesting("localhost", "/")

function assertSidebarItemsAndOptions(
const pinnedResourcesAccessor = accessorsForTesting<string[]>(
"pinned-resources"
)

export function assertSidebarItemsAndOptions(
root: ReactWrapper,
names: string[],
expectShowResources: boolean,
expectShowTests: boolean
expectShowTests: boolean,
expectAlertsOnTop: boolean
) {
let sidebar = root.find(SidebarResources)
expect(sidebar).toHaveLength(1)
Expand All @@ -27,133 +35,142 @@ function assertSidebarItemsAndOptions(
// or we'll have duplicates
let all = sidebar.find(SidebarListSection).find({ name: "resources" })
let items = all.find(SidebarItemView)
expect(items).toHaveLength(names.length)

for (let i = 0; i < names.length; i++) {
expect(items.at(i).props().item.name).toEqual(names[i])
}
const observedNames = items.map((i) => i.props().item.name)
expect(observedNames).toEqual(names)

let optSetter = sidebar.find(OverviewSidebarOptions)
expect(optSetter).toHaveLength(1)
expect(optSetter.find("input#resources").props().checked).toEqual(
expectShowResources
)
expect(optSetter.find("input#tests").props().checked).toEqual(expectShowTests)
expect(optSetter.find(AlertsOnTopToggle).hasClass("is-enabled")).toEqual(
expectAlertsOnTop
)
}

const allNames = ["(Tiltfile)", "vigoda", "snack", "beep", "boop"]

it("shows tests and resources by default", () => {
const root = mount(TwoResourcesTwoTests())
assertSidebarItemsAndOptions(root, allNames, true, true)
})

it("hides resources when resources unchecked", () => {
const root = mount(TwoResourcesTwoTests())
assertSidebarItemsAndOptions(root, allNames, true, true)

root
.find("input#resources")
.simulate("change", { target: { checked: false } })
assertSidebarItemsAndOptions(
root,
["(Tiltfile)", "beep", "boop"],
false,
true
)
})

it("hides tests when tests unchecked", () => {
const root = mount(TwoResourcesTwoTests())
assertSidebarItemsAndOptions(root, allNames, true, true)

root.find("input#tests").simulate("change", { target: { checked: false } })
assertSidebarItemsAndOptions(
root,
["(Tiltfile)", "vigoda", "snack"],
true,
false
)
})

it("hides resources and tests when both unchecked", () => {
const root = mount(TwoResourcesTwoTests())
assertSidebarItemsAndOptions(root, allNames, true, true)

root
.find("input#resources")
.simulate("change", { target: { checked: false } })
root.find("input#tests").simulate("change", { target: { checked: false } })
assertSidebarItemsAndOptions(root, ["(Tiltfile)"], false, false)
})

it("doesn't show SidebarOptionSetter if no tests present", () => {
let items = [tiltfileResource(), oneResource()].map((r) => new SidebarItem(r))
const root = mount(
<MemoryRouter>
<tiltfileKeyContext.Provider value="test">
<SidebarPinContextProvider>
<SidebarResources
items={items}
selected={""}
resourceView={ResourceView.Log}
pathBuilder={pathBuilder}
/>
</SidebarPinContextProvider>
</tiltfileKeyContext.Provider>
</MemoryRouter>
)
let sidebar = root.find(SidebarResources)
expect(sidebar).toHaveLength(1)

let optSetter = sidebar.find(OverviewSidebarOptions)
expect(optSetter).toHaveLength(0)
})

it("still displays pinned tests when tests hidden", () => {
localStorage.setItem(
makeKey("test", "pinned-resources"),
JSON.stringify(["beep"])
)
const root = mount(
<MemoryRouter>
<tiltfileKeyContext.Provider value="test">
<SidebarPinContextProvider>
{TwoResourcesTwoTests()}
</SidebarPinContextProvider>
</tiltfileKeyContext.Provider>
</MemoryRouter>
)

assertSidebarItemsAndOptions(
root,
["(Tiltfile)", "vigoda", "snack", "beep", "boop"],
true,
true
)

let pinned = root
.find(SidebarListSection)
.find({ name: "Pinned" })
.find(SidebarItemView)
expect(pinned).toHaveLength(1)
expect(pinned.at(0).props().item.name).toEqual("beep")

root.find("input#tests").simulate("change", { target: { checked: false } })
assertSidebarItemsAndOptions(
root,
["(Tiltfile)", "vigoda", "snack"],
true,
false
)

// "beep" should still be pinned, even though we're no longer showing tests in the main resource list
pinned = root
.find(SidebarListSection)
.find({ name: "Pinned" })
.find(SidebarItemView)
expect(pinned).toHaveLength(1)
expect(pinned.at(0).props().item.name).toEqual("beep")
describe("overview sidebar options", () => {
afterEach(() => {
localStorage.clear()
})

it("shows tests and resources by default", () => {
const root = mount(TwoResourcesTwoTests())
assertSidebarItemsAndOptions(root, allNames, true, true, false)
})

it("hides resources when resources unchecked", () => {
const root = mount(TwoResourcesTwoTests())
assertSidebarItemsAndOptions(root, allNames, true, true, false)

root
.find("input#resources")
.simulate("change", { target: { checked: false } })
assertSidebarItemsAndOptions(
root,
["(Tiltfile)", "beep", "boop"],
false,
true,
false
)
})

it("hides tests when tests unchecked", () => {
const root = mount(TwoResourcesTwoTests())
assertSidebarItemsAndOptions(root, allNames, true, true, false)

root.find("input#tests").simulate("change", { target: { checked: false } })
assertSidebarItemsAndOptions(
root,
["(Tiltfile)", "vigoda", "snack"],
true,
false,
false
)
})

it("hides resources and tests when both unchecked", () => {
const root = mount(TwoResourcesTwoTests())
assertSidebarItemsAndOptions(root, allNames, true, true, false)

root
.find("input#resources")
.simulate("change", { target: { checked: false } })
root.find("input#tests").simulate("change", { target: { checked: false } })
assertSidebarItemsAndOptions(root, ["(Tiltfile)"], false, false, false)
})

it("doesn't show SidebarOptionSetter if no tests present", () => {
let items = [tiltfileResource(), oneResource()].map(
(r) => new SidebarItem(r)
)
const root = mount(
<MemoryRouter>
<tiltfileKeyContext.Provider value="test">
<SidebarPinContextProvider>
<SidebarResources
items={items}
selected={""}
resourceView={ResourceView.Log}
pathBuilder={pathBuilder}
/>
</SidebarPinContextProvider>
</tiltfileKeyContext.Provider>
</MemoryRouter>
)
let sidebar = root.find(SidebarResources)
expect(sidebar).toHaveLength(1)

let optSetter = sidebar.find(OverviewSidebarOptions)
expect(optSetter).toHaveLength(0)
})

it("still displays pinned tests when tests hidden", () => {
pinnedResourcesAccessor.set(["beep"])
const root = mount(
<MemoryRouter>
<tiltfileKeyContext.Provider value="test">
<SidebarPinContextProvider>
{TwoResourcesTwoTests()}
</SidebarPinContextProvider>
</tiltfileKeyContext.Provider>
</MemoryRouter>
)

assertSidebarItemsAndOptions(
root,
["(Tiltfile)", "vigoda", "snack", "beep", "boop"],
true,
true,
false
)

let pinned = root
.find(SidebarListSection)
.find({ name: "Pinned" })
.find(SidebarItemView)
expect(pinned).toHaveLength(1)
expect(pinned.at(0).props().item.name).toEqual("beep")

root.find("input#tests").simulate("change", { target: { checked: false } })
assertSidebarItemsAndOptions(
root,
["(Tiltfile)", "vigoda", "snack"],
true,
false,
false
)

// "beep" should still be pinned, even though we're no longer showing tests in the main resource list
pinned = root
.find(SidebarListSection)
.find({ name: "Pinned" })
.find(SidebarItemView)
expect(pinned).toHaveLength(1)
expect(pinned.at(0).props().item.name).toEqual("beep")
})
})

// TODO:
Expand Down
Loading

0 comments on commit 195b562

Please sign in to comment.