Skip to content

Commit

Permalink
web: add zoom in/out for logs (#4354)
Browse files Browse the repository at this point in the history
Add buttons next to "Clear logs" to adjust the font size for logs.

At a high-level, a CSS custom property (variable) is used to set
a % font size for log lines (the absolute font size is set on the
container, and the default property value is 100%).

This provides an easy and performant way for JS to adjust the log
font size, particularly since the actual log CSS is pure CSS from
an SCSS stylesheet, not using `styled-components` in React.

The scale value is read from/written to local storage under a global
(NOT Tiltfile-namespaced) key so that it's persistent and applies to
all Tilt sessions. As a bonus, that means it live synchronizes between
tabs which is kinda nifty.
  • Loading branch information
milas committed Mar 23, 2021
1 parent 14bc7a0 commit e5a84e4
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 5 deletions.
2 changes: 1 addition & 1 deletion web/src/ClearLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ResourceName } from "./types"

const ClearLogsButton = styled.button`
${mixinResetButtonStyle}
margin-left: auto;
margin-left: 1rem;
font-size: ${FontSize.small};
color: ${Color.white};
transition: color ${AnimDuration.default} ease;
Expand Down
63 changes: 63 additions & 0 deletions web/src/LogActions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { mount } from "enzyme"
import React from "react"
import {
FontSizeDecreaseButton,
FontSizeIncreaseButton,
LogFontSizeScaleCSSProperty,
LogFontSizeScaleLocalStorageKey,
LogFontSizeScaleMinimumPercentage,
LogsFontSize,
} from "./LogActions"

describe("LogsFontSize", () => {
const cleanup = () => {
localStorage.clear()
document.documentElement.style.removeProperty("--log-font-scale")
}

beforeEach(() => {
cleanup()
// CSS won't be loaded in test context, so just explicitly set it
document.documentElement.style.setProperty("--log-font-scale", "100%")
})
afterEach(cleanup)

const getCSSValue = () =>
getComputedStyle(document.documentElement).getPropertyValue(
LogFontSizeScaleCSSProperty
)
// react-storage-hooks JSON (de)serializes transparently,
// need to do the same when directly manipulating local storage
const getLocalStorageValue = () =>
JSON.parse(localStorage.getItem(LogFontSizeScaleLocalStorageKey) || "")
const setLocalStorageValue = (val: string) =>
localStorage.setItem(LogFontSizeScaleLocalStorageKey, JSON.stringify(val))

it("restores persisted font scale on load", () => {
setLocalStorageValue("360%")
mount(<LogsFontSize />)
expect(getCSSValue()).toEqual("360%")
})

it("decreases font scale", () => {
const root = mount(<LogsFontSize />)
root.find(FontSizeDecreaseButton).simulate("click")
expect(getCSSValue()).toEqual("95%")
expect(getLocalStorageValue()).toEqual(`95%`) // JSON serialized
})

it("has a minimum font scale", () => {
setLocalStorageValue(`${LogFontSizeScaleMinimumPercentage}%`)
const root = mount(<LogsFontSize />)
root.find(FontSizeDecreaseButton).simulate("click")
expect(getCSSValue()).toEqual("10%")
expect(getLocalStorageValue()).toEqual(`10%`)
})

it("increases font scale", () => {
const root = mount(<LogsFontSize />)
root.find(FontSizeIncreaseButton).simulate("click")
expect(getCSSValue()).toEqual("105%")
expect(getLocalStorageValue()).toEqual(`105%`)
})
})
125 changes: 125 additions & 0 deletions web/src/LogActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, { useEffect } from "react"
import { useStorageState } from "react-storage-hooks"
import styled from "styled-components"
import { incr } from "./analytics"
import ClearLogs from "./ClearLogs"
import {
AnimDuration,
Color,
FontSize,
mixinResetButtonStyle,
} from "./style-helpers"

export const LogFontSizeScaleLocalStorageKey = "tilt.global.log-font-scale"
export const LogFontSizeScaleCSSProperty = "--log-font-scale"
export const LogFontSizeScaleMinimumPercentage = 10

const LogActionsGroup = styled.div`
margin-left: auto;
display: flex;
flex-direction: row;
justify-content: space-between;
`

const FontSizeControls = styled.div`
color: ${Color.gray6};
vertical-align: middle;
display: flex;
flex-wrap: nowrap;
`

const FontSizeControlsDivider = styled.div`
font-size: ${FontSize.default};
user-select: none;
`

const FontSizeButton = styled.button`
${mixinResetButtonStyle}
color: ${Color.gray6};
transition: color ${AnimDuration.default} ease;
padding: 0 4px;
user-select: none;
&:hover {
color: ${Color.blue};
}
`

export const FontSizeDecreaseButton = styled(FontSizeButton)`
font-size: ${FontSize.smallest};
`

export const FontSizeIncreaseButton = styled(FontSizeButton)`
font-size: ${FontSize.default};
`

export const LogsFontSize: React.FC = () => {
// this uses `useStorageState` directly vs `usePersistentState` wrapper because it's a global setting
// (i.e. log zoom applies across all Tiltfiles)
const [logFontScale, setLogFontSize] = useStorageState<string>(
localStorage,
LogFontSizeScaleLocalStorageKey,
() =>
document.documentElement.style.getPropertyValue(
LogFontSizeScaleCSSProperty
)
)
useEffect(() => {
if (!logFontScale?.endsWith("%")) {
// somehow an invalid value ended up in local storage - reset to 100% and let effect run again
setLogFontSize("100%")
return
}
document.documentElement.style.setProperty(
LogFontSizeScaleCSSProperty,
logFontScale
)
}, [logFontScale])

const adjustLogFontScale = (step: number) => {
const val = Math.max(
parseFloat(logFontScale) + step,
LogFontSizeScaleMinimumPercentage
)
setLogFontSize(`${val}%`)
incr("ui.web.zoomLogs", { action: "click", dir: step < 0 ? "out" : "in" })
}

const zoomStep = 5
return (
<FontSizeControls>
<FontSizeDecreaseButton
aria-label={"Decrease log font size"}
onClick={() => adjustLogFontScale(-zoomStep)}
>
A
</FontSizeDecreaseButton>
<FontSizeControlsDivider aria-hidden={true}>|</FontSizeControlsDivider>
<FontSizeIncreaseButton
aria-label={"Increase log font size"}
onClick={() => adjustLogFontScale(zoomStep)}
>
A
</FontSizeIncreaseButton>
</FontSizeControls>
)
}

export interface LogActionsProps {
resourceName: string
isSnapshot: boolean
}

const LogActions: React.FC<LogActionsProps> = ({
resourceName,
isSnapshot,
}) => {
return (
<LogActionsGroup>
<LogsFontSize />
{isSnapshot || <ClearLogs resourceName={resourceName} />}
</LogActionsGroup>
)
}

export default LogActions
10 changes: 9 additions & 1 deletion web/src/LogPaneLine.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@
// (styled-components wraps each React component with another, adding non-trivial overhead.)
@import "constants";

:root {
// to enable easy programmatic control of font scaling, the LogPaneRoot defines
// the absolute font size, and then scaling is controlled via a CSS variable, which
// is attached at the root so that it can be trivially manipulated via JS
// WARNING: this is relied upon by LogsFontSize component!
--log-font-scale: 100%;
}

.LogPaneLine {
display: flex;
position: relative;
font-size: $font-size-smallest;
font-size: var(--log-font-scale);

&.is-highlighted {
background-color: rgba($color-blue, $translucent);
Expand Down
5 changes: 3 additions & 2 deletions web/src/OverviewActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { incr } from "./analytics"
import { ReactComponent as CheckmarkSvg } from "./assets/svg/checkmark.svg"
import { ReactComponent as CopySvg } from "./assets/svg/copy.svg"
import { ReactComponent as LinkSvg } from "./assets/svg/link.svg"
import ClearLogs from "./ClearLogs"
import { displayURL } from "./links"
import LogActions from "./LogActions"
import { FilterLevel, FilterSet, FilterSource } from "./logfilters"
import { useLogStore } from "./LogStore"
import OverviewActionBarKeyboardShortcuts from "./OverviewActionBarKeyboardShortcuts"
Expand Down Expand Up @@ -375,6 +375,7 @@ export let ActionBarTopRow = styled.div`

let ActionBarBottomRow = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
border-bottom: 1px solid ${Color.grayLighter};
padding: ${SizeUnit(0.25)} ${SizeUnit(0.5)};
Expand Down Expand Up @@ -483,7 +484,7 @@ export default function OverviewActionBar(props: OverviewActionBarProps) {
filterSet={props.filterSet}
alerts={alerts}
/>
{isSnapshot || <ClearLogs resourceName={resourceName} />}
<LogActions resourceName={resourceName} isSnapshot={isSnapshot} />
</ActionBarBottomRow>
</ActionBarRoot>
)
Expand Down
4 changes: 3 additions & 1 deletion web/src/OverviewLogPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import LogStore, {
} from "./LogStore"
import PathBuilder, { usePathBuilder } from "./PathBuilder"
import { RafContext, useRaf } from "./raf"
import { Color, SizeUnit } from "./style-helpers"
import { Color, FontSize, SizeUnit } from "./style-helpers"
import { LogLevel, LogLine } from "./types"

// The number of lines to display before an error.
Expand All @@ -41,6 +41,7 @@ let LogPaneRoot = styled.section`
height: 100%;
overflow-y: auto;
box-sizing: border-box;
font-size: ${FontSize.smallest};
`

const blink = keyframes`
Expand All @@ -60,6 +61,7 @@ let LogEnd = styled.div`
animation-timing-function: ease;
padding-top: ${SizeUnit(0.25)};
padding-left: ${SizeUnit(0.625)};
font-size: var(--log-font-scale);
`

let anser = new Anser()
Expand Down

0 comments on commit e5a84e4

Please sign in to comment.