diff --git a/web/src/GlobalNav.tsx b/web/src/GlobalNav.tsx index b024a6b2b0..b1e7bd76a2 100644 --- a/web/src/GlobalNav.tsx +++ b/web/src/GlobalNav.tsx @@ -280,7 +280,6 @@ export function GlobalNav(props: GlobalNavProps) { open={helpDialogOpen} anchorEl={helpDialogAnchor} onClose={() => toggleHelpDialog(AnalyticsAction.Close)} - isOverview={true} /> ( - -) -export const DialogLegacy = () => ( - + ) diff --git a/web/src/HelpDialog.test.tsx b/web/src/HelpDialog.test.tsx index 5cd1684d8a..bd1341424d 100644 --- a/web/src/HelpDialog.test.tsx +++ b/web/src/HelpDialog.test.tsx @@ -1,7 +1,7 @@ import { mount } from "enzyme" import React from "react" import { MemoryRouter } from "react-router-dom" -import { DialogLegacy, DialogOverview } from "./HelpDialog.stories" +import { DialogOverview } from "./HelpDialog.stories" it("renders overview dialog", () => { mount( @@ -10,11 +10,3 @@ it("renders overview dialog", () => { ) }) - -it("renders legacy dialog", () => { - mount( - - - - ) -}) diff --git a/web/src/HelpDialog.tsx b/web/src/HelpDialog.tsx index 1d92554a6f..ed9f49cc53 100644 --- a/web/src/HelpDialog.tsx +++ b/web/src/HelpDialog.tsx @@ -10,7 +10,6 @@ type props = { open: boolean onClose: () => void anchorEl: Element | null - isOverview: boolean } let ShortcutRow = styled.div` @@ -126,16 +125,9 @@ export default function HelpDialog(props: props) { Shift + 1,{" "} 2 … - {props.isOverview ? null : ( - - - 1 - - - 2 - - - )} + + x + {cmdOrCtrlShortcut("Backspace")} s diff --git a/web/src/OverviewTable.tsx b/web/src/OverviewTable.tsx index b651e105bc..d2ff466532 100644 --- a/web/src/OverviewTable.tsx +++ b/web/src/OverviewTable.tsx @@ -3,7 +3,15 @@ import { AccordionDetails, AccordionSummary, } from "@material-ui/core" -import React, { ChangeEvent, MouseEvent, useMemo, useState } from "react" +import React, { + ChangeEvent, + MouseEvent, + MutableRefObject, + useEffect, + useMemo, + useRef, + useState, +} from "react" import { HeaderGroup, Row, @@ -40,6 +48,7 @@ import { rowIsDisabled, RowValues, } from "./OverviewTableColumns" +import { OverviewTableKeyboardShortcuts } from "./OverviewTableKeyboardShortcuts" import { AccordionDetailsStyleResetMixin, AccordionStyleResetMixin, @@ -63,6 +72,7 @@ import { Color, Font, FontSize, SizeUnit } from "./style-helpers" import { isZeroTime, timeDiff } from "./time" import { ResourceName, + ResourceStatus, TargetType, TriggerMode, UIButton, @@ -82,10 +92,12 @@ type TableWrapperProps = { type TableGroupProps = { label: string setGlobalSortBy: (id: string) => void + focused: string } & TableOptions type TableProps = { setGlobalSortBy?: (id: string) => void + focused: string } & TableOptions type ResourceTableHeadRowProps = { @@ -142,7 +154,7 @@ const ResourceTable = styled.table` } td:first-child { - padding-left: ${SizeUnit(0.75)}; + padding-left: 24px; } td:last-child { @@ -163,8 +175,16 @@ export const ResourceTableRow = styled.tr` color: ${Color.gray60}; padding-top: 6px; padding-bottom: 6px; + padding-left: 4px; - &.isDisabled { + &.isFocused, + &:focus { + border-left: 4px solid ${Color.blue}; + outline: none; + + td:first-child { + padding-left: 22px; + } } &.isSelected { @@ -454,6 +474,22 @@ function sortByDisableStatus(resources: UIResource[] = []) { return sorted } +function onlyEnabledRows(rows: RowValues[]): RowValues[] { + return rows.filter( + (row) => row.statusLine.runtimeStatus !== ResourceStatus.Disabled + ) +} +function onlyDisabledRows(rows: RowValues[]): RowValues[] { + return rows.filter( + (row) => row.statusLine.runtimeStatus === ResourceStatus.Disabled + ) +} +function enabledRowsFirst(rows: RowValues[]): RowValues[] { + let result = onlyEnabledRows(rows) + result.push(...onlyDisabledRows(rows)) + return result +} + export function labeledResourcesToTableCells( resources: UIResource[] | undefined, buttons: UIButton[] | undefined, @@ -592,6 +628,42 @@ function ShowMoreResourcesRow({ ) } +function TableRow(props: { row: Row; focused: string }) { + let { row, focused } = props + const { isSelected } = useResourceSelection() + let isFocused = row.original.name == focused + let rowClasses = + (rowIsDisabled(row) ? "isDisabled " : "") + + (isSelected(row.original.name) ? "isSelected " : "") + + (isFocused ? "isFocused " : "") + let ref: MutableRefObject = useRef(null) + + useEffect(() => { + if (isFocused && ref.current) { + ref.current.focus() + } + }, [isFocused, ref]) + + return ( + + {row.cells.map((cell) => ( + + {cell.render("Cell")} + + ))} + + ) +} + export function Table(props: TableProps) { if (props.data.length === 0) { return null @@ -619,7 +691,6 @@ export function Table(props: TableProps) { ) const showMoreOnClick = () => setPageSize(pageSize * RESOURCE_LIST_MULTIPLIER) - const { isSelected } = useResourceSelection() // TODO (lizz): Consider adding `aria-sort` markup to table headings return ( @@ -636,25 +707,12 @@ export function Table(props: TableProps) { {page.map((row: Row) => { prepareRow(row) - - let rowClasses = - (rowIsDisabled(row) ? "isDisabled " : "") + - (isSelected(row.original.name) ? "isSelected " : "") return ( - - {row.cells.map((cell) => ( - - {cell.render("Cell")} - - ))} - + ) })} labeledResourcesToTableCells(resources, buttons, logAlertIndex), [resources, buttons] ) + + const totalOrder = useMemo(() => { + let totalOrder = [] + data.labels.forEach((label) => + totalOrder.push(...enabledRowsFirst(data.labelsToResources[label])) + ) + totalOrder.push(...enabledRowsFirst(data.unlabeled)) + totalOrder.push(...enabledRowsFirst(data.tiltfile)) + return totalOrder + }, [data]) + let [focused, setFocused] = useState("") + const columns = getTableColumns(features) // Global table settings are currently used to sort multiple @@ -738,6 +808,7 @@ export function TableGroupedByLabels({ columns={columns} useControlledState={useControlledState} setGlobalSortBy={setGlobalSortBy} + focused={focused} /> ))} + ) @@ -768,13 +846,21 @@ export function TableWithoutGroups({ resources, buttons }: TableWrapperProps) { }, [resources, buttons]) const columns = getTableColumns(features) + let totalOrder = useMemo(() => enabledRowsFirst(data), [data]) + let [focused, setFocused] = useState("") + if (resources?.length === 0) { return null } return ( - +
+ ) } diff --git a/web/src/OverviewTableKeyboardShortcuts.tsx b/web/src/OverviewTableKeyboardShortcuts.tsx new file mode 100644 index 0000000000..85b1d54659 --- /dev/null +++ b/web/src/OverviewTableKeyboardShortcuts.tsx @@ -0,0 +1,106 @@ +import React, { Component } from "react" +import { RowValues } from "./OverviewTableColumns" +import { + ResourceSelectionContext, + useResourceSelection, +} from "./ResourceSelectionContext" +import { isTargetEditable } from "./shortcut" + +type Props = { + rows: RowValues[] + focused: string + setFocused: (focused: string) => void + selection: ResourceSelectionContext +} + +/** + * Sets up keyboard shortcuts that depend on the state of the sidebar. + */ +class Shortcuts extends Component { + constructor(props: Props) { + super(props) + this.onKeydown = this.onKeydown.bind(this) + } + + componentDidMount() { + document.body.addEventListener("keydown", this.onKeydown) + } + + componentWillUnmount() { + document.body.removeEventListener("keydown", this.onKeydown) + } + + onKeydown(e: KeyboardEvent) { + if (isTargetEditable(e) || e.shiftKey || e.altKey || e.isComposing) { + return + } + + let items = this.props.rows + let focused = this.props.focused + let dir = 0 + switch (e.key) { + case "Down": + case "ArrowDown": + case "j": + dir = 1 + break + + case "Up": + case "ArrowUp": + case "k": + dir = -1 + break + + case "x": + if (e.metaKey || e.ctrlKey) { + return + } + let item = items.find((item) => item.name == focused) + if (item && item.selectable) { + let selection = this.props.selection + if (selection.isSelected(item.name)) { + this.props.selection.deselect(item.name) + } else { + this.props.selection.select(item.name) + } + e.preventDefault() + } + break + } + + if (dir != 0) { + // Select up and down the list. + let names = items.map((item) => item.name) + let index = names.indexOf(focused) + let targetIndex = 0 + if (index != -1) { + let dir = e.key === "j" ? 1 : -1 + targetIndex = index + dir + } + + if (targetIndex < 0 || targetIndex >= names.length) { + return + } + + let name = names[targetIndex] + this.props.setFocused(name) + e.preventDefault() + return + } + } + + render() { + return + } +} + +type PublicProps = { + rows: RowValues[] + focused: string + setFocused: (focused: string) => void +} + +export function OverviewTableKeyboardShortcuts(props: PublicProps) { + let selection = useResourceSelection() + return +} diff --git a/web/src/ResourceSelectionContext.tsx b/web/src/ResourceSelectionContext.tsx index f932602e24..d85ef5352c 100644 --- a/web/src/ResourceSelectionContext.tsx +++ b/web/src/ResourceSelectionContext.tsx @@ -10,7 +10,7 @@ import { * The ResourceSelection state keeps track of what resources are selected for bulk actions to be performed on them. */ -type ResourceSelectionContext = { +export type ResourceSelectionContext = { selected: Set isSelected: (resourceName: string) => boolean select: (...resourceNames: string[]) => void diff --git a/web/storybook.tiltfile b/web/storybook.tiltfile index fc75b1206c..c663d927d4 100644 --- a/web/storybook.tiltfile +++ b/web/storybook.tiltfile @@ -30,10 +30,9 @@ local_resource( resource_deps=['install']) local_resource( - 'check:tsc', - 'node_modules/.bin/tsc -p .', + 'watch:tsc', + serve_cmd='yarn tsc -w', auto_init=False, - trigger_mode=TRIGGER_MODE_MANUAL, allow_parallel=True, labels=["lint"], resource_deps=['install'])