Skip to content

Commit

Permalink
web: add keyboard shortcut for clear logs (#4188)
Browse files Browse the repository at this point in the history
This is `Ctrl + Backspace` on Windows/Linux and `Cmd + Backspace`
on macOS.

N.B. It actually allows both `Ctrl` and `Meta` (Cmd/Win) on all OSes;
     this matches pre-existing conventions and is probably the most
     sane thing to do anyway as it's not hugely harmful and avoids
     browser OS detection issues.
  • Loading branch information
milas committed Feb 11, 2021
1 parent b6520fc commit 5e255a1
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 48 deletions.
7 changes: 3 additions & 4 deletions web/src/ClearLogs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import React from "react"
import ClearLogs from "./ClearLogs"
import { logLinesToString } from "./logs"
import LogStore, { LogStoreProvider } from "./LogStore"
import { oneResource } from "./testdata"
import { appendLinesForManifestAndSpan } from "./testlogs"
import { ResourceName } from "./types"

describe("ClearLogs", () => {
const createPopulatedLogStore = (): LogStore => {
Expand Down Expand Up @@ -36,7 +36,7 @@ describe("ClearLogs", () => {
const logStore = createPopulatedLogStore()
const root = mount(
<LogStoreProvider value={logStore}>
<ClearLogs />
<ClearLogs resourceName={ResourceName.all} />
</LogStoreProvider>
)
root.find(ClearLogs).simulate("click")
Expand All @@ -46,10 +46,9 @@ describe("ClearLogs", () => {

it("clears a specific resource", () => {
const logStore = createPopulatedLogStore()
const resource = oneResource()
const root = mount(
<LogStoreProvider value={logStore}>
<ClearLogs resource={resource} />
<ClearLogs resourceName={"vigoda"} />
</LogStoreProvider>
)
root.find(ClearLogs).simulate("click")
Expand Down
45 changes: 28 additions & 17 deletions web/src/ClearLogs.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from "react"
import styled from "styled-components"
import { useLogStore } from "./LogStore"
import { incr } from "./analytics"
import LogStore, { useLogStore } from "./LogStore"
import {
AnimDuration,
Color,
FontSize,
mixinResetButtonStyle,
} from "./style-helpers"
import { ResourceName } from "./types"

const ClearLogsButton = styled.button`
${mixinResetButtonStyle}
Expand All @@ -21,26 +23,35 @@ const ClearLogsButton = styled.button`
`

export interface ClearLogsProps {
resource?: Proto.webviewResource
resourceName: string
}

const ClearLogs: React.FC<ClearLogsProps> = ({ resource }) => {
const logStore = useLogStore()
const label = resource?.name ? "Clear Logs" : "Clear All Logs"

const clearLogs = () => {
let spans: { [key: string]: Proto.webviewLogSpan }
if (resource) {
const manifestName = resource.name ?? ""
spans = logStore.spansForManifest(manifestName)
} else {
spans = logStore.allSpans()
}

logStore.removeSpans(Object.keys(spans))
export const clearLogs = (
logStore: LogStore,
resourceName: string,
action: string
) => {
let spans: { [key: string]: Proto.webviewLogSpan }
const all = resourceName === ResourceName.all
if (all) {
spans = logStore.allSpans()
} else {
spans = logStore.spansForManifest(resourceName)
}
incr("ui.web.clearLogs", { action, all: all.toString() })
logStore.removeSpans(Object.keys(spans))
}

return <ClearLogsButton onClick={() => clearLogs()}>{label}</ClearLogsButton>
const ClearLogs: React.FC<ClearLogsProps> = ({ resourceName }) => {
const logStore = useLogStore()
const label =
resourceName == ResourceName.all ? "Clear All Logs" : "Clear Logs"

return (
<ClearLogsButton onClick={() => clearLogs(logStore, resourceName, "click")}>
{label}
</ClearLogsButton>
)
}

export default ClearLogs
17 changes: 11 additions & 6 deletions web/src/OverviewActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import { ReactComponent as LinkSvg } from "./assets/svg/link.svg"
import ClearLogs from "./ClearLogs"
import { displayURL } from "./links"
import { FilterLevel, FilterSet, FilterSource } from "./logfilters"
import { useLogStore } from "./LogStore"
import OverviewActionBarKeyboardShortcuts from "./OverviewActionBarKeyboardShortcuts"
import { usePathBuilder } from "./PathBuilder"
import { AnimDuration, Color, Font, FontSize, SizeUnit } from "./style-helpers"
import { ResourceName } from "./types"

type OverviewActionBarProps = {
// The current resource. May be null if there is no resource.
Expand Down Expand Up @@ -415,10 +417,11 @@ function openEndpointUrl(url: string) {

export default function OverviewActionBar(props: OverviewActionBarProps) {
let { resource, filterSet, alerts } = props
let manifestName = resource?.name || ""
let endpoints = resource?.endpointLinks || []
let podId = resource?.podID || ""
const resourceName = resource ? resource.name || "" : ResourceName.all
const isSnapshot = usePathBuilder().isSnapshot()
const logStore = useLogStore()

let endpointEls: any = []
endpoints.forEach((ep, i) => {
Expand Down Expand Up @@ -452,15 +455,17 @@ export default function OverviewActionBar(props: OverviewActionBarProps) {
<EndpointSet />
)}
{copyButton}
<OverviewActionBarKeyboardShortcuts
endpoints={endpoints}
openEndpointUrl={openEndpointUrl}
/>
</ActionBarTopRow>
) : null

return (
<ActionBarRoot>
<OverviewActionBarKeyboardShortcuts
logStore={logStore}
resourceName={resourceName}
endpoints={endpoints}
openEndpointUrl={openEndpointUrl}
/>
{topRow}
<ActionBarBottomRow>
<FilterRadioButton
Expand All @@ -478,7 +483,7 @@ export default function OverviewActionBar(props: OverviewActionBarProps) {
filterSet={props.filterSet}
alerts={alerts}
/>
{isSnapshot || <ClearLogs resource={resource} />}
{isSnapshot || <ClearLogs resourceName={resourceName} />}
</ActionBarBottomRow>
</ActionBarRoot>
)
Expand Down
70 changes: 51 additions & 19 deletions web/src/OverviewActionBarKeyboardShortcuts.test.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { fireEvent } from "@testing-library/dom"
import { mount } from "enzyme"
import React from "react"
import { logLinesToString } from "./logs"
import LogStore from "./LogStore"
import OverviewActionBarKeyboardShortcuts from "./OverviewActionBarKeyboardShortcuts"
import { appendLinesForManifestAndSpan } from "./testlogs"

function numKeyCode(num: number): number {
return num + 48
}

type Link = Proto.webviewLink

let logStore: LogStore | null
let component: any
let endpointUrl = ""
const shortcuts = (endpoints: Link[]) => {
logStore = new LogStore()
endpointUrl = ""
component = mount(
<OverviewActionBarKeyboardShortcuts
logStore={logStore}
resourceName={"fake-resource"}
endpoints={endpoints}
openEndpointUrl={(url) => (endpointUrl = url)}
/>
Expand All @@ -26,26 +33,51 @@ afterEach(() => {
component.unmount()
component = null
}
if (logStore) {
logStore = null
}
})

it("zero endpoint urls", () => {
shortcuts([])
fireEvent.keyDown(document.body, { keyCode: numKeyCode(1), shiftKey: true })
expect(endpointUrl).toEqual("")
})
it("two endpoint urls trigger first", () => {
shortcuts([
{ url: "https://tilt.dev:4000" },
{ url: "https://tilt.dev:4001" },
])
fireEvent.keyDown(document.body, { keyCode: numKeyCode(1), shiftKey: true })
expect(endpointUrl).toEqual("https://tilt.dev:4000")
describe("endpoints", () => {
it("zero endpoint urls", () => {
shortcuts([])
fireEvent.keyDown(document.body, { keyCode: numKeyCode(1), shiftKey: true })
expect(endpointUrl).toEqual("")
})
it("two endpoint urls trigger first", () => {
shortcuts([
{ url: "https://tilt.dev:4000" },
{ url: "https://tilt.dev:4001" },
])
fireEvent.keyDown(document.body, { keyCode: numKeyCode(1), shiftKey: true })
expect(endpointUrl).toEqual("https://tilt.dev:4000")
})
it("two endpoint urls trigger second", () => {
shortcuts([
{ url: "https://tilt.dev:4000" },
{ url: "https://tilt.dev:4001" },
])
fireEvent.keyDown(document.body, { keyCode: numKeyCode(2), shiftKey: true })
expect(endpointUrl).toEqual("https://tilt.dev:4001")
})
})
it("two endpoint urls trigger second", () => {
shortcuts([
{ url: "https://tilt.dev:4000" },
{ url: "https://tilt.dev:4001" },
])
fireEvent.keyDown(document.body, { keyCode: numKeyCode(2), shiftKey: true })
expect(endpointUrl).toEqual("https://tilt.dev:4001")

describe("clears logs", () => {
it("meta key", () => {
shortcuts([])
appendLinesForManifestAndSpan(logStore!, "fake-resource", "span:1", [
"line 1\n",
])
fireEvent.keyDown(document.body, { key: "Backspace", metaKey: true })
expect(logLinesToString(logStore!.allLog(), false)).toEqual("")
})

it("ctrl key", () => {
shortcuts([])
appendLinesForManifestAndSpan(logStore!, "fake-resource", "span:1", [
"line 1\n",
])
fireEvent.keyDown(document.body, { key: "Backspace", ctrlKey: true })
expect(logLinesToString(logStore!.allLog(), false)).toEqual("")
})
})
18 changes: 16 additions & 2 deletions web/src/OverviewActionBarKeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React, { Component } from "react"
import { incr } from "./analytics"
import { clearLogs } from "./ClearLogs"
import LogStore from "./LogStore"

type Link = Proto.webviewLink

type Props = {
logStore: LogStore
resourceName: string
endpoints?: Link[]
openEndpointUrl: (url: string) => void
}
Expand All @@ -29,7 +33,17 @@ class OverviewActionBarKeyboardShortcuts extends Component<Props> {
}

onKeydown(e: KeyboardEvent) {
if (e.metaKey || e.altKey || e.ctrlKey || e.isComposing) {
if (e.altKey || e.isComposing) {
return
}

if (e.ctrlKey || e.metaKey) {
if (e.key === "Backspace" && !e.shiftKey) {
clearLogs(this.props.logStore, this.props.resourceName, "shortcut")
e.preventDefault()
return
}

return
}

Expand All @@ -48,7 +62,7 @@ class OverviewActionBarKeyboardShortcuts extends Component<Props> {
}

render() {
return <span style={{ display: "none" }}></span>
return <></>
}
}

Expand Down
21 changes: 21 additions & 0 deletions web/src/ShortcutsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ function Shortcut(props: React.PropsWithChildren<{ label: string }>) {
)
}

function cmdOrCtrlShortcut(key: string) {
// OS detection is inherently fragile on the web; thankfully, we really only
// care about macOS vs "everything else" and as of macOS 11 (Big Sur), this
// works reliably
const isMac = navigator.platform.indexOf("Mac") != -1

if (isMac) {
return (
<>
<ShortcutBox>&#8984;</ShortcutBox> + <ShortcutBox>{key}</ShortcutBox>
</>
)
}
return (
<>
<ShortcutBox>Ctrl</ShortcutBox> + <ShortcutBox>{key}</ShortcutBox>
</>
)
}

export default function ShortcutsDialog(props: props) {
return (
<FloatDialog id="shortcuts" title="Keyboard Shortcuts" {...props}>
Expand All @@ -81,6 +101,7 @@ export default function ShortcutsDialog(props: props) {
</Shortcut>
</React.Fragment>
)}
<Shortcut label="Clear Logs">{cmdOrCtrlShortcut("Backspace")}</Shortcut>
<Shortcut label="Make Snapshot">
<ShortcutBox>s</ShortcutBox>
</Shortcut>
Expand Down

0 comments on commit 5e255a1

Please sign in to comment.