Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions e2e/tests/console/notebookHighlight.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/// <reference types="cypress" />

// E2E coverage for result grid highlight rules in a notebook cell: the drawer
// saves rules, a re-run flashes changed cells with a direction glyph, and a
// value rule colors a cell on the first run.

const openHighlightDrawer = () => {
cy.get("[data-notebook-cell] button[aria-label='More actions']").click()
cy.contains("[role='menuitem']", "Highlight rules").click()
cy.getByDataHook("highlight-settings-drawer").should("be.visible")
}

const pickOption = (rule, label, option) => {
cy.wrap(rule).find(`button[aria-label^="${label}"]`).click()
cy.contains("[role^='menuitem']", option).click()
}

const pickColumn = (rule, column) => {
cy.wrap(rule).find("button[aria-label='Column']").click()
cy.contains("[role='option']", new RegExp(`^${column}$`)).click()
}

const addRule = (column, condition) => {
cy.contains("button", "+ Add rule").click()
cy.getByDataHook("highlight-rule")
.last()
.then(($rule) => {
pickColumn($rule, column)
pickOption($rule, "Condition", condition)
})
}

const runCell = () => {
cy.get("[data-notebook-cell] button[aria-label='Run cell']").click()
cy.get("[data-notebook-cell] [data-hook='grid-cell']").should("exist")
}

describe("notebook highlight rules", () => {
beforeEach(() => {
cy.loadConsoleWithAuth()
cy.getEditorContent().should("be.visible")
cy.createNotebook()
cy.focusNotebookCell()
})

it("flashes changed values with a direction glyph after up and down rules are saved", () => {
// Given a cell whose value changes on every run
cy.focused().type("select 'A' as k, rnd_double() as v", { delay: 0 })
runCell()

// When an up rule and a down rule on v are saved
openHighlightDrawer()
cy.getByDataHook("highlight-identity-status").should("not.exist")
addRule("v", "> previous")
addRule("v", "< previous")
cy.getByDataHook("highlight-rule").should("have.length", 2)
cy.contains("button", "Save").click()
cy.getByDataHook("highlight-settings-drawer").should("not.exist")

// And the cell runs again
cy.get("[data-notebook-cell] button[aria-label='Re-run query']").click()

// Then the changed cell flashes and shows a direction
cy.get("[data-hook='grid-cell'][data-highlight='temporary']").should(
"have.length",
1,
)
cy.get("[data-hook='grid-cell'][data-direction]").should("have.length", 1)
openHighlightDrawer()
cy.getByDataHook("highlight-identity-status").should("not.exist")
})

it("colors a cell that passes a value rule without a previous result", () => {
// Given a cell with a constant value
cy.focused().type("select 'A' as k, 5 as v", { delay: 0 })
runCell()

// When a "> value" rule on v is saved with a threshold of 1
openHighlightDrawer()
addRule("v", "> value")
cy.getByDataHook("highlight-rule").within(() => {
cy.get("[aria-label='Value']").clear().type("1")
})
cy.contains("button", "Save").click()

// Then the cell is highlighted as a persistent match
cy.get("[data-hook='grid-cell'][data-highlight='always']").should(
"have.length",
1,
)

// And Clear all removes the rules and the badge
openHighlightDrawer()
cy.contains("button", "Clear all").click()
cy.get("[data-hook='grid-cell'][data-highlight]").should("not.exist")
})

it("colors every cell of the row when a rule applies to the row", () => {
// Given a cell with two columns and a constant value
cy.focused().type("select 'A' as k, 5 as v", { delay: 0 })
runCell()

// When a "> value" rule on v is saved with "Applies to" set to Row
openHighlightDrawer()
addRule("v", "> value")
cy.getByDataHook("highlight-rule").within(() => {
cy.get("[aria-label='Value']").clear().type("1")
})
cy.getByDataHook("highlight-rule").then(($rule) =>
pickOption($rule, "Applies to", "Row"),
)
cy.contains("button", "Save").click()

// Then both cells of the row are highlighted
cy.get("[data-hook='grid-row'][data-highlight-row]").should(
"have.length",
1,
)
cy.get("[data-hook='grid-cell'][data-highlight='always']").should(
"have.length",
2,
)
})
})
26 changes: 14 additions & 12 deletions src/components/Checkbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { Check } from "@phosphor-icons/react"
import styled from "styled-components"
import { statusInfoFocus } from "../../theme"

type Props = React.InputHTMLAttributes<HTMLInputElement>
type Props = React.InputHTMLAttributes<HTMLInputElement> & {
compact?: boolean
}

const Indicator = styled.span`
const Indicator = styled.span<{ $compact: boolean }>`
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
width: 100%;
height: 100%;
box-sizing: border-box;
border: 1px solid ${({ theme }) => theme.color.borderStrong};
border-radius: 0.4rem;
Expand All @@ -22,8 +24,8 @@ const Indicator = styled.span`
box-shadow 120ms ease;

svg {
width: 1.3rem;
height: 1.3rem;
width: ${({ $compact }) => ($compact ? "1rem" : "1.3rem")};
height: ${({ $compact }) => ($compact ? "1rem" : "1.3rem")};
opacity: 0;
transform: scale(0.72);
transition:
Expand Down Expand Up @@ -75,22 +77,22 @@ const NativeCheckbox = styled.input`
}
`

const Root = styled.span`
const Root = styled.span<{ $compact: boolean }>`
position: relative;
display: inline-flex;
flex: 0 0 auto;
width: 1.8rem;
height: 1.8rem;
width: ${({ $compact }) => ($compact ? "1.4rem" : "1.8rem")};
height: ${({ $compact }) => ($compact ? "1.4rem" : "1.8rem")};
vertical-align: middle;
`

export const Checkbox: React.FunctionComponent<Props> = forwardRef<
HTMLInputElement,
Props
>((props, ref) => (
<Root>
>(({ compact = false, ...props }, ref) => (
<Root $compact={compact}>
<NativeCheckbox ref={ref} type="checkbox" {...props} />
<Indicator aria-hidden="true">
<Indicator aria-hidden="true" $compact={compact}>
<Check weight="bold" />
</Indicator>
</Root>
Expand Down
72 changes: 72 additions & 0 deletions src/components/ColorPalette/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from "react"
import styled, { useTheme, type DefaultTheme } from "styled-components"
import { Check } from "../icons"
import { ButtonBase } from "../Button"
import { pickReadableTextColor } from "../../utils"

export type ThemeColorToken = keyof DefaultTheme["color"]

const Root = styled.div`
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.5rem;
`

const ColorBox = styled(ButtonBase)`
position: relative;
width: 1.6rem;
height: 1.6rem;
padding: 0;
border: 0;
cursor: pointer;
`

const CheckIcon = styled(Check)`
position: absolute;
`

type Props<Token extends ThemeColorToken> = {
tokens: readonly Token[]
selectedToken: Token
onSelect: (token: Token) => void
labelPrefix?: string
labelFor?: (token: Token) => string
}

export const ColorPalette = <Token extends ThemeColorToken>({
tokens,
selectedToken,
onSelect,
labelPrefix = "Color",
labelFor,
}: Props<Token>) => {
const theme = useTheme()

return (
<Root>
{tokens.map((token, index) => (
<ColorBox
key={token}
aria-label={
labelFor ? labelFor(token) : `${labelPrefix} ${index + 1}`
}
title={labelFor ? labelFor(token) : undefined}
aria-pressed={selectedToken === token}
style={{ backgroundColor: theme.color[token] }}
onClick={() => onSelect(token)}
>
{selectedToken === token && (
<CheckIcon
size="16px"
color={pickReadableTextColor(theme.color[token], [
theme.color.contentInverse,
theme.color.neutralInk,
])}
/>
)}
</ColorBox>
))}
</Root>
)
}
Loading
Loading