diff --git a/.eslintrc.yml b/.eslintrc.yml index 02736c6256..b53a00db28 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -66,3 +66,5 @@ rules: prettier/prettier: - error - endOfLine: auto + react/react-in-jsx-scope: + - off diff --git a/.node-version b/.node-version index 34326d62e6..56bfee434b 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v16.8.0 +v16.10.0 diff --git a/CODE.md b/CODE.md new file mode 100644 index 0000000000..1844ea61ca --- /dev/null +++ b/CODE.md @@ -0,0 +1,51 @@ +# Coding Patterns + +This is here to document the design patterns chosen by the developers. It documents structures, abstractions, and philosophy in this repo. + +## Folders + +Documentation for where code should go. + +### src/domain + +Domains are concepts specific to Zui. Things like pools, queries, sessions, history. A domain can contain operations, handlers, a plugin-api, models, types. + +### src/core + +Core objects and functions can be used across multiple domains. Like menus and commands. + +### src/util + +This is super generic JavaScript code that knows nothing about Zui, Electron, or Web. It can be copied into another project and work right out of the gate. Code must contain no dependencies in this folder. + +### src/plugins + +This is a directory of plugins that use the plugin api to add functionality to the app. Some of the plugins are prefixed with the word "core" to indicate they provide core functionality, but only require the plugin api to achieve this. To add a plugin today, create a new directory in the plugins directory, create an `index.ts` file within it, then export a named function called "activate". It will accept the PluginContext object as its only argument. Then go to the `run-plugins.ts` file and call the activate function within the body of runPlugins. This could be made automatic one day, but it's hardcoded for now. + +## Glossary of Terms + +In no particular order. + +_Domain_ + +A named concept relative to the Zui app. Examples of domains are pools, queries, panes, configurations, plugins. First-class things that people can point to when they describe the app. + +_Main Object_ + +The main object contains methods for managing the application in the main process. It can manage windows, session data, plugins, and app lifecycle. + +_Operation_ + +A function that runs in the main process and has the main object in scope. When the app boots, operations begin listening for IPC invocations from a renderer process. They can also be called directly from the main process using their .run() method. When invoking an operation from the main process, use the invoke function found in src/core/invoke.ts. + +_Handler_ + +A handler is an event listener in the renderer process. It waits for a message from the main process before running its callback function. Messages can be sent to handlers from the main process using sendToFocusedWindow(message, ...arguments). + +_Message_ + +A message is a TypeScript type defining a name and a set of arguments. Messages are necessary to provide types for Operations and Handlers. + +_Plugin Api_ + +The plugin api runs in the Node main process and is given to plugin authors to extend the app. It should not have privileged access to everything. Only what is useful for plugin authors. This means it should not contain references to the full store or the main process. Instead, it exposes methods that in turn call operations. Operations are not exposed to plugins and therefore have the main object and store in scope. The pattern should be: plugin-api exposes a simple method which then runs an operation. diff --git a/package.json b/package.json index 9f0d6c28d7..67d41b99fb 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ }, "devDependencies": { "@babel/core": "^7.17.9", - "@brimdata/zed-js": "0.0.16", - "@brimdata/zed-node": "0.0.16", + "@brimdata/zed-js": "0.0.17", + "@brimdata/zed-node": "0.0.17", "@codemirror/autocomplete": "^0.19.15", "@codemirror/closebrackets": "^0.19.1", "@codemirror/commands": "^0.19.8", @@ -67,7 +67,7 @@ "@testing-library/user-event": "^14.1.1", "@types/animejs": "^3.1.2", "@types/classnames": "^2.2.10", - "@types/d3": "^5.7.2", + "@types/d3": "^6.7.0", "@types/electron-devtools-installer": "^2.2.0", "@types/fs-extra": "^9.0.1", "@types/lodash": "^4.14.161", @@ -83,8 +83,8 @@ "@types/sprintf-js": "^1.1.2", "@types/styled-components": "^5.1.3", "@types/tmp": "^0.2.0", - "@typescript-eslint/eslint-plugin": "^5.16.0", - "@typescript-eslint/parser": "^5.16.0", + "@typescript-eslint/eslint-plugin": "5.60.1", + "@typescript-eslint/parser": "5.60.1", "abort-controller": "^3.0.0", "acorn": "^7.4.1", "ajv": "^6.9.1", @@ -96,7 +96,7 @@ "classnames": "^2.2.6", "commander": "^2.20.3", "cross-fetch": "^3.1.6", - "d3": "^5.16.0", + "d3": "^6.7.0", "date-fns": "^2.16.1", "decompress": "^4.2.1", "electron": "^22.0.0", @@ -146,11 +146,14 @@ "react-dnd": "^14.0.5", "react-dnd-html5-backend": "^14.0.2", "react-dom": "^18.0.0", + "react-error-boundary": "^4.0.10", + "react-hook-form": "^7.44.3", "react-hot-toast": "^1.0.1", "react-input-autosize": "^3.0.0", "react-is": "^17.0.2", "react-markdown": "^6.0.2", "react-redux": "^8.0.5", + "react-resizable-panels": "^0.0.45", "react-router": "5.3.1", "react-spring": "^8.0.27", "react-tooltip": "^4.2.7", @@ -166,7 +169,7 @@ "styled-components": "^5.3.5", "tmp": "^0.1.0", "tree-model": "^1.0.7", - "typescript": "^4.6.2", + "typescript": "5.1.5", "use-resize-observer": "^8.0.0", "web-file-polyfill": "^1.0.4", "web-streams-polyfill": "^3.2.0", diff --git a/packages/e2e-tests/tests/queries.spec.ts b/packages/e2e-tests/tests/queries.spec.ts index 122156854a..ba2f0eeb98 100644 --- a/packages/e2e-tests/tests/queries.spec.ts +++ b/packages/e2e-tests/tests/queries.spec.ts @@ -28,8 +28,12 @@ test.describe("Query tests", () => { const entries = await history.evaluateAll((nodes) => nodes.map((n) => n.innerText.trim().replaceAll(/\s+/g, " ")) ) - - expect(entries).toEqual(["3 now", "2 now", "1 now"]) + const expected = [ + "from 'sample.tsv' | 3 now", + "from 'sample.tsv' | 2 now", + "from 'sample.tsv' | 1 now", + ] + expect(entries).toEqual(expected) }) test("named queries' creation, modification, update/save, proper outdated status display", async () => { diff --git a/src/app/commands/pins.ts b/src/app/commands/pins.ts index 7d7d01a602..25d471df5f 100644 --- a/src/app/commands/pins.ts +++ b/src/app/commands/pins.ts @@ -6,6 +6,8 @@ import {TimeRangeQueryPin} from "src/js/state/Editor/types" import Pools from "src/js/state/Pools" import submitSearch from "../query-home/flows/submit-search" import {createCommand} from "./command" +import Current from "src/js/state/Current" +import PoolSettings from "src/js/state/PoolSettings" export const createFromEditor = createCommand( "pins.createFromEditor", @@ -69,10 +71,12 @@ export const createTimeRange = createCommand( async ({dispatch, api, getState}) => { const pins = Editor.getPins(getState()) const [from, to] = await defaultRange(api) + const poolId = Current.getPoolFromQuery(getState())?.id + const {timeField} = PoolSettings.findWithDefaults(getState(), poolId) dispatch( Editor.addPin({ type: "time-range", - field: "ts", + field: timeField, from: from.toISOString(), to: to.toISOString(), }) diff --git a/src/app/features/right-pane/history/history-item.tsx b/src/app/features/right-pane/history/history-item.tsx index 0dd61fff6b..6c7ff91882 100644 --- a/src/app/features/right-pane/history/history-item.tsx +++ b/src/app/features/right-pane/history/history-item.tsx @@ -93,7 +93,7 @@ function getValue(active: ActiveQuery) { if (active.isDeleted()) { return "(Deleted)" } else if (active.isAnonymous() || active.isModified()) { - return active.value() || "(Empty)" + return active.toZed() || "(Empty)" } else { return active.name() } diff --git a/src/app/query-home/flows/submit-search.ts b/src/app/query-home/flows/submit-search.ts index 56e10d91fe..3633bf8172 100644 --- a/src/app/query-home/flows/submit-search.ts +++ b/src/app/query-home/flows/submit-search.ts @@ -2,9 +2,9 @@ import Current from "src/js/state/Current" import Editor from "src/js/state/Editor" import Results from "src/js/state/Results" import QueryVersions from "../../../js/state/QueryVersions" -import {MAIN_RESULTS} from "src/js/state/Results/types" import {Thunk} from "src/js/state/types" import {QueryModel} from "../../../js/models/query-model" +import {RESULTS_QUERY} from "src/panes/results-pane/run-results-query" const submitSearch = (): Thunk => @@ -16,7 +16,7 @@ const submitSearch = // An error with the syntax if (error) { const tabId = Current.getTabId(getState()) - dispatch(Results.error({id: MAIN_RESULTS, error, tabId})) + dispatch(Results.error({id: RESULTS_QUERY, error, tabId})) return } diff --git a/src/app/query-home/histogram/ChartSVG.tsx b/src/app/query-home/histogram/ChartSVG.tsx deleted file mode 100644 index 618e775812..0000000000 --- a/src/app/query-home/histogram/ChartSVG.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, {useLayoutEffect, useRef} from "react" - -import {Pen} from "./types" - -type Props = { - chart: { - pens: Pen[] - width: number - height: number - } -} - -const ChartSVG = React.memo(function ChartSVG({chart}: Props) { - const el = useRef(null) - - function mount() { - const node = el.current - if (node) chart.pens.forEach((pen) => pen.mount(node)) - } - - function draw() { - chart.pens.forEach((pen) => pen.draw(chart, draw)) - } - - useLayoutEffect(mount, [el.current]) - useLayoutEffect(draw) - - return ( - - ) -}) - -export default ChartSVG diff --git a/src/app/query-home/histogram/MainHistogram/Chart.tsx b/src/app/query-home/histogram/MainHistogram/Chart.tsx deleted file mode 100644 index 03b74e2d50..0000000000 --- a/src/app/query-home/histogram/MainHistogram/Chart.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react" -import {useSelector} from "react-redux" -import Dimens from "src/js/components/Dimens" -import {DateTuple} from "src/js/lib/TimeWindow" -import Histogram from "src/js/state/Histogram" -import Layout from "src/js/state/Layout" -import styled from "styled-components" - -import ChartSVG from "../ChartSVG" -import useMainHistogram from "./useMainHistogram" - -const BG = styled.div` - height: 100px; - border-bottom: 1px solid var(--border-color); -` - -export default function MainHistogramChart() { - const show = useSelector(Layout.getShowHistogram) - const range = useSelector(Histogram.getRange) - if (!range) return null - if (!show) return null - return ( - - ( - - )} - /> - - ) -} - -export type HistogramProps = {height: number; width: number; range: DateTuple} - -function MainHistogramSvg(props: HistogramProps) { - const chart = useMainHistogram(props) - return -} diff --git a/src/app/query-home/histogram/MainHistogram/format.ts b/src/app/query-home/histogram/MainHistogram/format.ts deleted file mode 100644 index 2f5b283f8a..0000000000 --- a/src/app/query-home/histogram/MainHistogram/format.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {ChartData} from "src/js/state/Chart/types" -import {DateTuple} from "src/js/lib/TimeWindow" -import {HistogramData} from "../types" -import histogramInterval from "src/js/lib/histogramInterval" - -export type HistogramDataPoint = { - ts: Date - paths: { - [key: string]: number - } - count: number -} - -export default function format( - data: ChartData, - range: DateTuple -): HistogramData { - const interval = histogramInterval(range) - - const defaults: { - [key: string]: number - } = data.keys.reduce((obj, path) => ({...obj, [path]: 0}), {}) - - const bins = [] - const times = Object.keys(data.table).map((ms) => { - const epochTs = parseInt(ms) - const ts = new Date(epochTs) - bins.push({ - ts, - paths: { - ...defaults, - ...data.table[ms], - }, - count: Object.values(data.table[ms]).reduce((c, sum) => sum + c, 0), - }) - return epochTs - }) - const spanStart = new Date(Math.min(...times, range[0].getTime())) - return { - interval, - span: [spanStart, range[1]], - points: bins, - keys: data.keys, - } -} diff --git a/src/app/query-home/histogram/MainHistogram/useMainHistogram.tsx b/src/app/query-home/histogram/MainHistogram/useMainHistogram.tsx deleted file mode 100644 index 113e47b3b5..0000000000 --- a/src/app/query-home/histogram/MainHistogram/useMainHistogram.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import {useSelector} from "react-redux" -import {useDispatch} from "src/app/core/state" -import React, {useMemo} from "react" -import * as d3 from "d3" -import {DateTuple} from "src/js/lib/TimeWindow" -import {Pen, HistogramChart} from "../types" -import {innerHeight, innerWidth} from "../dimens" - -import EmptyMessage from "src/js/components/EmptyMessage" -import HistogramTooltip from "src/js/components/HistogramTooltip" -import LoadingMessage from "src/js/components/LoadingMessage" -import barStacks from "../pens/barStacks" -import format from "./format" -import hoverLine from "../pens/hoverLine" -import reactComponent from "../pens/reactComponent" -import useConst from "src/js/components/hooks/useConst" -import xAxisBrush from "../pens/xAxisBrush" -import xAxisTime from "../pens/xAxisTime" -import xPositionTooltip from "../pens/xPositionTooltip" -import yAxisSingleTick from "../pens/yAxisSingleTick" -import submitSearch from "../../flows/submit-search" -import Results from "src/js/state/Results" -import {ChartData} from "src/js/state/Chart/types" -import * as zed from "@brimdata/zed-js" -import UniqArray from "src/js/models/UniqArray" -import MergeHash from "src/js/models/MergeHash" -import Editor from "src/js/state/Editor" -import {HISTOGRAM_RESULTS} from "src/js/state/Histogram/run-query" -import {HistogramProps} from "./Chart" - -const id = HISTOGRAM_RESULTS - -// get pool -// make a new query with the values, -// get the pool name -// get the pool -// get the full pool range - -export default function useMainHistogram( - props: HistogramProps -): HistogramChart { - const {height, width, range} = props - const dispatch = useDispatch() - const chartData = useSelector(Results.getValues(id)) as zed.Record[] - const status = useSelector(Results.getStatus(id)) - - const pens = useConst([], () => { - function onDragEnd(span: DateTuple) { - const [from, to] = span - dispatch(Editor.setTimeRange({field: "ts", from, to})) - dispatch(submitSearch()) - } - - function onSelection(span: DateTuple) { - const [from, to] = span - dispatch(Editor.setTimeRange({field: "ts", from, to})) - dispatch(submitSearch()) - } - - return [ - xAxisTime({onDragEnd}), - barStacks(), - yAxisSingleTick(), - xAxisBrush({onSelection}), - hoverLine(), - reactComponent((chart) => ( - - )), - reactComponent((chart) => ( - - )), - xPositionTooltip({ - wrapperClassName: "histogram-tooltip-wrapper", - render: HistogramTooltip, - }), - ] - }) - - return useMemo(() => { - const data = format(histogramFormat(chartData), range) - const maxY = d3.max(data.points, (d: {count: number}) => d.count) || 0 - const margins = { - left: 24, - right: 24, - top: 20, - bottom: 28, - } - return { - data, - width, - height, - margins, - state: { - isFetching: status === "FETCHING", - isEmpty: data.points.length === 0, - isDragging: false, - }, - yScale: d3 - .scaleLinear() - .range([innerHeight(height, margins), 0]) - .domain([0, maxY]), - xScale: d3 - .scaleUtc() - .range([0, innerWidth(width, margins)]) - .domain(data.span), - pens, - } - }, [chartData, status, range, width, height]) -} - -function histogramFormat(records: zed.Record[]): ChartData { - const paths = new UniqArray() - const table = new MergeHash() - - records.forEach((r) => { - const [ts, path, count] = r.fields.map((f) => f.data) as [ - zed.Time, - zed.String, - zed.Uint64 - ] - - try { - const pathName = path.toString() - const key = ts.toDate().getTime() - const val = {[path.toString()]: count.toInt()} - - table.merge(key, val) - paths.push(pathName) - } catch (e) { - console.log("Error rendering histogram: " + e.toString()) - } - }) - - return { - table: table.toJSON(), - keys: paths.toArray(), - } -} diff --git a/src/app/query-home/histogram/dimens.ts b/src/app/query-home/histogram/dimens.ts deleted file mode 100644 index 0aaf4f8378..0000000000 --- a/src/app/query-home/histogram/dimens.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {Margins} from "./types" - -export function innerHeight(height: number, margins: Margins) { - return Math.max(height - margins.top - margins.bottom, 0) -} - -export function innerWidth(width: number, margins: Margins) { - return Math.max(width - margins.left - margins.right, 0) -} diff --git a/src/app/query-home/histogram/getPointAt.ts b/src/app/query-home/histogram/getPointAt.ts deleted file mode 100644 index 3e660c8188..0000000000 --- a/src/app/query-home/histogram/getPointAt.ts +++ /dev/null @@ -1,14 +0,0 @@ -import time from "src/js/models/time" -import {Chart} from "./types" - -export const getPointAt = (left: number, chart: Chart) => { - const ts = chart.xScale.invert(left - chart.margins.left) - const {number, unit} = chart.data.interval - for (let index = 0; index < chart.data.points.length; index++) { - const point = chart.data.points[index] - const nextTs = time(point.ts).add(number, unit).toDate() - if (ts >= point.ts && ts < nextTs) return point - } - - return null -} diff --git a/src/app/query-home/histogram/pens/barStacks.ts b/src/app/query-home/histogram/pens/barStacks.ts deleted file mode 100644 index fa55cb6c59..0000000000 --- a/src/app/query-home/histogram/pens/barStacks.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as d3 from "d3" - -import {Pen} from "../types" -import {innerHeight, innerWidth} from "../dimens" -import time from "src/js/models/time" - -export default function (): Pen { - let chartG - function mount(svg) { - chartG = d3.select(svg).append("g").attr("class", "chart") - } - - function draw(chart) { - const series = d3 - .stack() - .keys(chart.data.keys) - .value((d, key) => d.paths[key])(chart.data.points) - - const barGroups = chartG - .attr( - "transform", - `translate(${chart.margins.left}, ${chart.margins.top})` - ) - .selectAll("g") - .data(series, (d) => d.key) - - const t = d3.transition().duration(500) - const innerH = innerHeight(chart.height, chart.margins) - barGroups.exit().selectAll("rect").remove() - - const bars = barGroups - .enter() - .append("g") - .attr("class", (d) => `${d.key}-bg-color`) - .merge(barGroups) - .selectAll("rect") - .data((d) => d) - - bars.exit().attr("opacity", 1).attr("y", innerH).attr("opacity", 0).remove() - - let width = 0 - if (chart.data.points[0]) { - const ts = chart.data.points[0].ts - const {number, unit} = chart.data.interval - const a = chart.xScale(ts) - const b = chart.xScale(time(ts).add(number, unit).toDate()) - width = Math.max(Math.floor(b - a), 1) - } - - function clampWidth(d) { - // Keep the chart from overflowing the x axis - const chartWidth = innerWidth(chart.width, chart.margins) - const x = chart.xScale(d.data.ts) - if (x < 0) - // The leftmost bar has overflowed - return Math.max(0, width + x) - else if (x + width > chartWidth) - // Right most bar has overflowed - return Math.max(0, width - (x + width - chartWidth)) - // The bar is within the bounds - else return width - } - - bars - .enter() - .append("rect") - .attr("y", innerH) - .attr("height", 0) - .merge(bars) - .attr("width", clampWidth) - .attr("x", (d) => Math.max(0, chart.xScale(d.data.ts))) - .transition(t) - .attr("y", (d) => chart.yScale(d[1])) - .attr("height", (d) => chart.yScale(d[0]) - chart.yScale(d[1])) - } - - return {mount, draw} -} diff --git a/src/app/query-home/histogram/pens/hoverLine.ts b/src/app/query-home/histogram/pens/hoverLine.ts deleted file mode 100644 index 07a769ad0f..0000000000 --- a/src/app/query-home/histogram/pens/hoverLine.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as d3 from "d3" - -import {Pen} from "../types" -import {innerHeight} from "../dimens" - -export default function (): Pen { - let line - const overflow = 10 - let svg - - function mount(el) { - svg = el - line = d3 - .select(svg) - .insert("rect") - .attr("class", "hover-line") - .style("pointer-events", "none") - .style("display", "none") - .attr("width", "1px") - } - - function draw(chart) { - line.attr("height", innerHeight(chart.height, chart.margins) + overflow * 2) - - function hide() { - line.style("display", "none") - } - - function show(this: d3.ContainerElement) { - if (chart.state.isDragging) return hide() - - const [x] = d3.mouse(this) - if (x < chart.margins.left) { - line.style("display", "none") - } else { - line - .attr("transform", `translate(${x}, ${chart.margins.top - overflow})`) - .style("display", "block") - } - } - - d3.select(svg) - .on("mouseout.hoverline", hide) - .on("mousemove.hoverline", show) - .on("mousedown.hoverline", hide) - } - - return {mount, draw} -} diff --git a/src/app/query-home/histogram/pens/reactComponent.ts b/src/app/query-home/histogram/pens/reactComponent.ts deleted file mode 100644 index 9921ea61a7..0000000000 --- a/src/app/query-home/histogram/pens/reactComponent.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {createRoot} from "react-dom/client" - -import {Pen} from "../types" - -export default function reactComponent(renderComponent: any): Pen { - let root - - function mount(el) { - const container = document.createElement("div") - root = createRoot(container) - if (el.parentNode) { - el.parentNode.appendChild(container) - } - } - - function draw(chart) { - root.render(renderComponent(chart)) - } - - return {draw, mount} -} diff --git a/src/app/query-home/histogram/pens/xAxisBrush.ts b/src/app/query-home/histogram/pens/xAxisBrush.ts deleted file mode 100644 index 5a3d5f391a..0000000000 --- a/src/app/query-home/histogram/pens/xAxisBrush.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {isEqual} from "lodash" -import * as d3 from "d3" - -import {DateSpan, Pen} from "../types" -import {innerHeight, innerWidth} from "../dimens" - -type Props = { - onSelection?: (arg0: DateSpan) => void -} - -export default function (props: Props = {}): Pen { - const {onSelection} = props - let brushG - - function mount(svg) { - brushG = d3.select(svg).append("g").attr("class", "brush") - } - - function draw(chart) { - let prevSelection = null - - function onBrushStart() { - prevSelection = d3.brushSelection(brushG.node()) - } - - function onBrushEnd(this: d3.ContainerElement) { - const {selection, sourceEvent} = d3.event - - if (!sourceEvent) { - return - } - - if (!selection) { - return - } - - if (!isEqual(selection, prevSelection)) { - onSelection && onSelection(selection.map(chart.xScale.invert)) - return - } - } - - brushG.attr( - "transform", - `translate(${chart.margins.left}, ${chart.margins.top})` - ) - const brush = d3.brushX().extent([ - [0, 0], - [ - innerWidth(chart.width, chart.margins), - innerHeight(chart.height, chart.margins), - ], - ]) - - brushG.call(brush) - chart.state.selection - ? brush.move(brushG, chart.state.selection.map(chart.xScale)) - : brush.move(brushG, null) - - brush.on("end", onBrushEnd) - brush.on("start", onBrushStart) - } - - return {mount, draw} -} diff --git a/src/app/query-home/histogram/pens/xAxisTime.ts b/src/app/query-home/histogram/pens/xAxisTime.ts deleted file mode 100644 index efefc582e8..0000000000 --- a/src/app/query-home/histogram/pens/xAxisTime.ts +++ /dev/null @@ -1,87 +0,0 @@ -import {isEqual} from "lodash" -import * as d3 from "d3" - -import {DateSpan, Pen} from "../types" -import {duration, shift} from "src/js/lib/TimeWindow" -import {innerWidth} from "../dimens" - -type Props = { - onDragEnd: (arg0: DateSpan) => void -} - -export default function ({onDragEnd}: Props): Pen { - let startSpan = null - let startPos = null - let xAxis - let dragArea - - function mount(svg) { - xAxis = d3.select(svg).append("g").attr("class", "x-axis") - - // Make the invisible rect the size of the x axis to listen for the drag - dragArea = xAxis - .append("rect") - .attr("class", "x-axis-drag") - .attr("fill", "transparent") - } - - function draw(chart, redraw) { - function getXPos() { - return d3.mouse(xAxis.node())[0] - } - - function addListeners() { - d3.select("body").on("mousemove", drag, true).on("mouseup", dragEnd) - } - - function removeListeners() { - d3.select("body").on("mousemove", null).on("mouseup", null) - } - - function draggedSpan(chart, _startX, startSpan): DateSpan { - const pos = getXPos() - const [from, to] = [pos, startPos].map(chart.xScale.invert) - const diff = duration([from, to]) - return shift(startSpan, diff) - } - - function dragEnd() { - if (startPos === null || startSpan === null) return - removeListeners() - onDragEnd(draggedSpan(chart, startPos, startSpan)) - chart.state.isDragging = false - startPos = null - startSpan = null - } - - function drag() { - if (startPos === null || startSpan === null) return - const nextSpan = draggedSpan(chart, startPos, startSpan) - const currSpan = chart.xScale.domain() - if (!isEqual(nextSpan, currSpan)) { - chart.xScale.domain(nextSpan) - redraw(chart) - } - } - - function dragStart() { - startPos = getXPos() - startSpan = chart.data.span - chart.state.isDragging = true - addListeners() - } - - const x = chart.margins.left - const y = chart.height - chart.margins.bottom - xAxis - .attr("transform", `translate(${x}, ${y})`) - .call(d3.axisBottom(chart.xScale)) - - dragArea - .attr("width", innerWidth(chart.width, chart.margins)) - .attr("height", chart.margins.bottom) - .on("mousedown", dragStart) - } - - return {mount, draw} -} diff --git a/src/app/query-home/histogram/pens/xPositionTooltip.test.ts b/src/app/query-home/histogram/pens/xPositionTooltip.test.ts deleted file mode 100644 index 380f8e34ec..0000000000 --- a/src/app/query-home/histogram/pens/xPositionTooltip.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {xPosition} from "./xPositionTooltip" - -describe("#xPosition", () => { - let parentWidth, padding, width - beforeEach(() => { - parentWidth = 1000 - padding = 20 - width = 100 - }) - - test("right of the mouse", () => { - expect(xPosition(0, width, parentWidth, padding)).toBe("20px") - expect(xPosition(879, width, parentWidth, padding)).toBe("899px") - }) - - test("left of the mouse", () => { - expect(xPosition(880, width, parentWidth, padding)).toBe("760px") - expect(xPosition(1000, width, parentWidth, padding)).toBe("880px") - }) -}) diff --git a/src/app/query-home/histogram/pens/xPositionTooltip.tsx b/src/app/query-home/histogram/pens/xPositionTooltip.tsx deleted file mode 100644 index 2d8848ebe2..0000000000 --- a/src/app/query-home/histogram/pens/xPositionTooltip.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import {isEqual} from "lodash" -import {select, mouse, ContainerElement} from "d3" -import React from "react" - -import {HistogramDataPoint} from "../MainHistogram/format" -import {Pen} from "../types" -import {getPointAt} from "../getPointAt" -import {createRoot} from "react-dom/client" - -type Args = { - wrapperClassName: string - render: any -} - -export default function ({wrapperClassName, render: Component}: Args): Pen { - let div - let svg - let lastPoint - let root - - function hide() { - div.style.opacity = "0" - } - - function mount(el) { - svg = el - div = document.createElement("div") - root = createRoot(div) - - div.classList.add(wrapperClassName) - if (svg.parentNode) svg.parentNode.appendChild(div) - select(svg).select(".brush").on("mousedown.tooltip", hide) - } - - function draw(chart) { - function show() { - if (chart.state.isDragging) return hide() - - const [left] = mouse(svg) - const point = getPointAt(left, chart) - - if (point && point.count) { - positionTooltip(div, svg, 30) - if (!isEqual(lastPoint, point)) { - root.render() - } - lastPoint = point - } else { - hide() - } - } - - select(svg) - .select(".brush") - .on("mouseout.tooltip", hide) - .on("mousemove.tooltip", show) - } - - return {mount, draw} -} - -const getProps = (point: HistogramDataPoint) => { - const segments = [] - const paths = point.paths - for (const key in paths) { - if (paths[key] !== 0) segments.push([key, paths[key]]) - } - - return {ts: point.ts, segments} -} - -export const positionTooltip = ( - el: HTMLElement, - parent: ContainerElement, - padding: number -) => { - const [left] = mouse(parent) - const {width} = el.getBoundingClientRect() - const {width: parentWidth} = parent.getBoundingClientRect() - - select(el) - .style("left", xPosition(left, width, parentWidth, padding)) - .style("opacity", "1") -} - -export const xPosition = ( - left: number, - width: number, - parentWidth: number, - padding: number -) => { - if (left + width + padding >= parentWidth) { - return left - width - padding + "px" - } else { - return left + padding + "px" - } -} diff --git a/src/app/query-home/histogram/pens/yAxisSingleTick.ts b/src/app/query-home/histogram/pens/yAxisSingleTick.ts deleted file mode 100644 index 19d99ede17..0000000000 --- a/src/app/query-home/histogram/pens/yAxisSingleTick.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as d3 from "d3" - -import {Pen} from "../types" - -export default function (): Pen { - let yaxis - - function mount(svg) { - yaxis = d3.select(svg).append("g").attr("class", "y-axis-single-tick") - } - - function draw(chart) { - if (chart.data.points.length === 0) { - yaxis.style("opacity", "0") - return - } - - yaxis - .attr( - "transform", - `translate(${chart.margins.left}, ${chart.margins.top})` - ) - .style("opacity", "1") - .call(d3.axisRight(chart.yScale).tickValues([chart.yScale.domain()[1]])) - } - - return {mount, draw} -} diff --git a/src/app/query-home/histogram/types.ts b/src/app/query-home/histogram/types.ts deleted file mode 100644 index 3b28568776..0000000000 --- a/src/app/query-home/histogram/types.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {Interval} from "src/js/lib/histogramInterval" -export type DateSpan = [Date, Date] - -export type Margins = { - top: number - left: number - right: number - bottom: number -} - -type Redraw = (arg0: any) => void -type PenFunc = (arg0: any, arg1: Redraw) => void - -export type Pen = { - draw: PenFunc - mount: (arg0: Element) => void -} - -type HistogramState = { - selection?: DateSpan | null | undefined - isFetching?: boolean - isEmpty?: boolean - isDragging: boolean -} - -export type HistogramData = { - points: { - ts: Date - paths: { - [key: string]: number - } - count: number - }[] - keys: string[] - interval: Interval - span: DateSpan -} - -export type HistogramChart = { - height: number - width: number - margins: Margins - data: HistogramData - state: HistogramState - yScale: d3.ScaleLinear - xScale: d3.ScaleTime - pens: Pen[] -} - -export type Chart = HistogramChart diff --git a/src/app/query-home/index.tsx b/src/app/query-home/index.tsx index e1d18b8edb..9a59bec58a 100644 --- a/src/app/query-home/index.tsx +++ b/src/app/query-home/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useContext, useState} from "react" +import React, {useCallback, useContext, useMemo, useState} from "react" import {useSelector} from "react-redux" import Current from "src/js/state/Current" @@ -9,10 +9,15 @@ import RightPane from "../features/right-pane" import {TitleBar} from "./title-bar/title-bar" import {ResultsToolbar} from "./toolbar/results-toolbar" import {Redirect} from "react-router" -import MainHistogramChart from "./histogram/MainHistogram/Chart" import {ActiveQuery} from "../core/models/active-query" import {ResultsPane} from "src/panes/results-pane/results-pane" import {TableViewApi} from "src/zui-kit/core/table-view/table-view-api" +import {HistogramPane} from "src/panes/histogram-pane/pane" +import {Panel, PanelGroup, PanelResizeHandle} from "react-resizable-panels" +import styles from "./query-home.module.css" +import {useDispatch} from "../core/state" +import Layout from "src/js/state/Layout" +import useSelect from "../core/hooks/use-select" const MainContent = styled.div` display: flex; @@ -59,6 +64,8 @@ const QueryHome = () => { const activeQuery = useSelector(Current.getActiveQuery) const lakeId = useSelector(Current.getLakeId) const tabId = useSelector(Current.getTabId) + const select = useSelect() + const dispatch = useDispatch() if (activeQuery.isDeleted()) { return ( @@ -68,15 +75,38 @@ const QueryHome = () => { ) } + const panelStorage = useMemo(() => { + return { + setItem(_: string, value: string) { + dispatch(Layout.setQueryPanels(value)) + }, + getItem(_: string) { + return select(Layout.getQueryPanels) + }, + } + }, []) + return ( - - - - - + + + + + + + + + + + + diff --git a/src/app/query-home/loader.ts b/src/app/query-home/loader.ts index 42ab14807f..f22058c9fd 100644 --- a/src/app/query-home/loader.ts +++ b/src/app/query-home/loader.ts @@ -1,24 +1,22 @@ import Current from "src/js/state/Current" import Editor from "src/js/state/Editor" import {syncPool} from "../core/pools/sync-pool" -import Results from "src/js/state/Results" import {startTransition} from "react" import {QueryModel} from "../../js/models/query-model" -import {MAIN_RESULTS} from "src/js/state/Results/types" import Notice from "src/js/state/Notice" import Tabs from "src/js/state/Tabs" import {Thunk} from "src/js/state/types" import {Location} from "history" -import {runHistogramQuery} from "src/js/state/Histogram/run-query" import Pools from "src/js/state/Pools" import {invoke} from "src/core/invoke" +import {runHistogramQuery} from "src/panes/histogram-pane/run-query" +import {runResultsQuery} from "src/panes/results-pane/run-results-query" export function loadRoute(location: Location): Thunk { return (dispatch) => { dispatch(syncPluginContext) dispatch(Tabs.loaded(location.key)) dispatch(Notice.dismiss()) - dispatch(Results.error({id: MAIN_RESULTS, error: null, tabId: ""})) dispatch(syncEditor) dispatch(fetchData()) } @@ -46,13 +44,13 @@ function syncEditor(dispatch, getState) { } function fetchData() { - return (dispatch, getState) => { + return (dispatch, getState, {api}) => { const version = Current.getVersion(getState()) startTransition(() => { if (version) { - dispatch(Results.fetchFirstPage(QueryModel.versionToZed(version))) - dispatch(runHistogramQuery()) + dispatch(runResultsQuery()) + runHistogramQuery(api) } }) } diff --git a/src/app/query-home/query-home.module.css b/src/app/query-home/query-home.module.css new file mode 100644 index 0000000000..c665c15597 --- /dev/null +++ b/src/app/query-home/query-home.module.css @@ -0,0 +1,31 @@ +.borderResizeHandle { + width: 100%; + height: 11px; + position: relative; +} + +.borderResizeHandle:after { + content: ""; + position: absolute; + background: var(--border-color); + top: 5px; + left: 0; + right: 0; + height: 1px; +} + +.invisibleResizeHandle { + width: 100%; + height: 11px; + position: relative; + transform: translateY(5px); +} + +.resultsToolbar { + border-top: 1px solid var(--border-color); +} + +.panel { + display: flex; + flex-direction: column; +} diff --git a/src/app/query-home/results/data-hook.ts b/src/app/query-home/results/data-hook.ts index 90043d2616..ca4d03cb9b 100644 --- a/src/app/query-home/results/data-hook.ts +++ b/src/app/query-home/results/data-hook.ts @@ -1,11 +1,11 @@ import {useDeferredValue} from "react" import {useSelector} from "react-redux" import Results from "src/js/state/Results" -import {MAIN_RESULTS} from "src/js/state/Results/types" +import {RESULTS_QUERY} from "src/panes/results-pane/run-results-query" export function useResultsData() { - const values = useSelector(Results.getValues(MAIN_RESULTS)) - const shapes = useSelector(Results.getShapes(MAIN_RESULTS)) + const values = useSelector(Results.getValues(RESULTS_QUERY)) + const shapes = useSelector(Results.getShapes(RESULTS_QUERY)) return { values: useDeferredValue(values), diff --git a/src/app/query-home/search-area/Input.tsx b/src/app/query-home/search-area/Input.tsx index 7772030e5f..bc5e914741 100644 --- a/src/app/query-home/search-area/Input.tsx +++ b/src/app/query-home/search-area/Input.tsx @@ -1,28 +1,10 @@ import React from "react" import styled from "styled-components" import QueryEditor from "./editor/query-editor" -import {useDispatch} from "../../core/state" -import useDrag, {DragArgs} from "src/app/core/hooks/use-drag" -import Editor from "../../../js/state/Editor" -import {useSelector} from "react-redux" -import lib from "src/js/lib" -const DragAnchor = styled.div` - position: absolute; - background: transparent; - pointer-events: all !important; - z-index: 99; - width: 100%; - height: 16px; - bottom: -8px; - top: unset; - cursor: row-resize; -` - -const InputBackdrop = styled.div<{height: number}>` - border-bottom: 1px solid var(--border-color); +const InputBackdrop = styled.div` position: relative; - height: ${(p) => p.height + "px"}; + height: 100%; ` type Props = { @@ -31,21 +13,9 @@ type Props = { } export default function Input({value, disabled}: Props) { - const dispatch = useDispatch() - const height = useSelector(Editor.getHeight) - - const onDrag = ({dy}: DragArgs) => { - const minH = 54 - const maxH = lib.win.getHeight() - 400 - const newHeight = height + dy - dispatch(Editor.setHeight(Math.max(Math.min(newHeight, maxH), minH))) - } - const bindDrag = useDrag(onDrag) - return ( - + - ) } diff --git a/src/app/query-home/search-area/index.tsx b/src/app/query-home/search-area/index.tsx index b9fbd8fabb..9267869f8d 100644 --- a/src/app/query-home/search-area/index.tsx +++ b/src/app/query-home/search-area/index.tsx @@ -11,6 +11,7 @@ const Group = styled.div` display: flex; flex-direction: column; padding: 0; + height: 100%; ` export default function SearchArea() { diff --git a/src/app/query-home/search-area/pins/base-pin.tsx b/src/app/query-home/search-area/pins/base-pin.tsx index b7c2866ebd..d38e4df780 100644 --- a/src/app/query-home/search-area/pins/base-pin.tsx +++ b/src/app/query-home/search-area/pins/base-pin.tsx @@ -70,7 +70,7 @@ export const Prefix = styled.span` const Label = styled.span` overflow: hidden; text-overflow: ellipsis; - line-height: 15px; + line-height: 17px; ` const Dropdown = styled(Icon).attrs({name: "chevron-down"})` diff --git a/src/app/query-home/toolbar/results-toolbar.tsx b/src/app/query-home/toolbar/results-toolbar.tsx index 8ad091a262..36f53f538d 100644 --- a/src/app/query-home/toolbar/results-toolbar.tsx +++ b/src/app/query-home/toolbar/results-toolbar.tsx @@ -3,12 +3,13 @@ import {ButtonMenu} from "src/components/button-menu" import {Toolbar} from "src/components/toolbar" import {ResultsViewSwitch} from "./results-view-switch" import {useMenuInstance} from "src/core/menu/use-menu-instance" +import styles from "../query-home.module.css" export function ResultsToolbar() { const menu = useMenuInstance("results.toolbarMenu") return ( - + diff --git a/src/components/auto-size.tsx b/src/components/auto-size.tsx new file mode 100644 index 0000000000..9046d9ff73 --- /dev/null +++ b/src/components/auto-size.tsx @@ -0,0 +1,17 @@ +import useResizeObserver from "use-resize-observer" + +const parentStyles = { + display: "flex", + outline: "1px solid blue", + background: "red", + height: "100%", +} + +export function AutoSize(props: {children: any}) { + const {ref, width = 1, height = 1} = useResizeObserver() + return ( +
+ {props.children({width, height})} +
+ ) +} diff --git a/src/components/dialog/click-is-within-element.ts b/src/components/dialog/click-is-within-element.ts new file mode 100644 index 0000000000..4e919c79a1 --- /dev/null +++ b/src/components/dialog/click-is-within-element.ts @@ -0,0 +1,9 @@ +export function clickIsWithinElement(e: MouseEvent, el: HTMLElement) { + const rect = el.getBoundingClientRect() + return ( + e.clientX > rect.left && + e.clientX < rect.right && + e.clientY > rect.top && + e.clientY < rect.bottom + ) +} diff --git a/src/components/dialog/dialog.tsx b/src/components/dialog/dialog.tsx new file mode 100644 index 0000000000..3449e014c5 --- /dev/null +++ b/src/components/dialog/dialog.tsx @@ -0,0 +1,34 @@ +import {useRef, MouseEventHandler} from "react" +import {usePosition} from "./use-position" +import useListener from "src/js/components/hooks/useListener" +import {useOpener} from "./use-opener" +import {useOutsideClick} from "./use-outside-click" + +export type DialogProps = { + isOpen: boolean + onClose: () => void + modal?: boolean + onOutsideClick?: (e: globalThis.MouseEvent) => void + onClick?: MouseEventHandler + children?: any + className?: string + anchor?: HTMLElement + anchorPoint?: string + dialogPoint?: string + dialogMargin?: string + keepOnScreen?: boolean +} + +export function Dialog(props: DialogProps) { + const ref = useRef() + const style = usePosition(ref.current, props) + useOpener(ref.current, props) + useOutsideClick(ref.current, props) + useListener(ref.current, "close", props.onClose) + + return ( + + {props.children} + + ) +} diff --git a/src/components/dialog/parse-margin.ts b/src/components/dialog/parse-margin.ts new file mode 100644 index 0000000000..5c21a5847f --- /dev/null +++ b/src/components/dialog/parse-margin.ts @@ -0,0 +1,27 @@ +export function parseMargin(s: string) { + const parts = s + .split(/\s+/) + .map((s) => s.trim()) + .map(toPixels) + if (parts.length === 0) throw new Error("Invalid margin") + if (parts.length === 1) { + return {left: parts[0], right: parts[0], top: parts[0], bottom: parts[0]} + } + if (parts.length === 2) { + return {top: parts[0], bottom: parts[0], left: parts[1], right: parts[1]} + } + if (parts.length === 3) { + return {top: parts[0], right: parts[1], left: parts[1], bottom: parts[2]} + } + if (parts.length === 4) { + return {top: parts[0], right: parts[1], left: parts[2], bottom: parts[3]} + } + throw new Error("Invalid margin") +} + +function toPixels(s: string) { + if (s === "0") return 0 + if (/\d+px/.test(s)) return parseInt(s) + + throw new Error("Only pixel values accepted") +} diff --git a/src/components/dialog/parse-point.test.ts b/src/components/dialog/parse-point.test.ts new file mode 100644 index 0000000000..6b7b9a0470 --- /dev/null +++ b/src/components/dialog/parse-point.test.ts @@ -0,0 +1,25 @@ +import {parsePoint} from "./parse-point" + +test("left top", () => { + expect(parsePoint("left top")).toEqual(["left", "top"]) +}) + +test("center left", () => { + expect(parsePoint("center left")).toEqual(["left", "center"]) +}) + +test("center right", () => { + expect(parsePoint("center right")).toEqual(["right", "center"]) +}) + +test("right bottom", () => { + expect(parsePoint("right bottom")).toEqual(["right", "bottom"]) +}) + +test("bottom center", () => { + expect(parsePoint("bottom center")).toEqual(["center", "bottom"]) +}) + +test("10% 20%", () => { + expect(parsePoint("10% 20%")).toEqual(["10%", "20%"]) +}) diff --git a/src/components/dialog/parse-point.ts b/src/components/dialog/parse-point.ts new file mode 100644 index 0000000000..69ddb51361 --- /dev/null +++ b/src/components/dialog/parse-point.ts @@ -0,0 +1,14 @@ +export function parsePoint(point: string): [string, string] { + const words = point.split(/\s+/).map((s) => s.trim()) + if (words.length === 0) throw new Error("No words passed to point") + if (words.length === 1) throw new Error("Must pass two words to point") + if (words.length > 2) throw new Error("Too many words passed to point") + + return words.sort((a, b) => { + if (a === "left" || a === "right") return -1 + if (a === "top" || a === "bottom") return 1 + if (b === "top" || b === "bottom") return -1 + if (b === "left" || b === "right") return 1 + return 0 + }) as [string, string] +} diff --git a/src/components/dialog/use-opener.ts b/src/components/dialog/use-opener.ts new file mode 100644 index 0000000000..c93b557201 --- /dev/null +++ b/src/components/dialog/use-opener.ts @@ -0,0 +1,13 @@ +import {useEffect} from "react" +import {DialogProps} from "./dialog" + +export function useOpener(dialog: HTMLDialogElement, props: DialogProps) { + useEffect(() => { + if (!dialog) return + if (props.isOpen && !dialog.open) { + props.modal ? dialog.showModal() : dialog.show() + } else { + dialog.close() + } + }, [props.isOpen, props.modal]) +} diff --git a/src/components/dialog/use-outside-click.ts b/src/components/dialog/use-outside-click.ts new file mode 100644 index 0000000000..86c43f49ca --- /dev/null +++ b/src/components/dialog/use-outside-click.ts @@ -0,0 +1,27 @@ +import {useEffect, useRef} from "react" +import {clickIsWithinElement} from "./click-is-within-element" +import {DialogProps} from "./dialog" + +export function useOutsideClick(dialog: HTMLDialogElement, props: DialogProps) { + const callback = useRef<(e: globalThis.MouseEvent) => void>(() => {}) + + useEffect(() => { + callback.current = (e: globalThis.MouseEvent) => { + if (clickIsWithinElement(e, dialog)) return + props.onOutsideClick && props.onOutsideClick(e) + } + }, [dialog, props.onOutsideClick]) + + useEffect(() => { + let tid: any + const listener = (e: globalThis.MouseEvent) => callback.current(e) + const add = () => document.addEventListener("mousedown", listener) + const remove = () => document.removeEventListener("mousedown", listener) + + if (props.isOpen) tid = setTimeout(add) + return () => { + clearTimeout(tid) + remove() + } + }, [dialog, props.isOpen]) +} diff --git a/src/components/dialog/use-position.ts b/src/components/dialog/use-position.ts new file mode 100644 index 0000000000..0052613283 --- /dev/null +++ b/src/components/dialog/use-position.ts @@ -0,0 +1,133 @@ +import {useLayoutEffect, useState} from "react" +import useListener from "src/js/components/hooks/useListener" +import {DialogProps} from "./dialog" +import {CSSProperties} from "react" +import {parsePoint} from "./parse-point" +import {parseMargin} from "./parse-margin" + +export function usePosition(dialog: HTMLDialogElement, props: DialogProps) { + const doc = document.documentElement + const anchor = props.anchor ?? doc + const anchorPoint = props.anchorPoint ?? "center center" + const dialogPoint = props.dialogPoint ?? "center center" + const dialogMargin = props.dialogMargin ?? "0 0 0 0" + const keepOnScreen = props.keepOnScreen ?? true + const [position, setPosition] = useState({ + top: 0, + left: 0, + position: "fixed", + }) + + const run = () => { + if (!props.isOpen || !dialog) return + const anchorRect = anchor.getBoundingClientRect() + const dialogRect = dialog.getBoundingClientRect() + let left = anchorRect.left + let top = anchorRect.top + const leftMin = 0 + const leftMax = doc.clientWidth - leftMin + const topMin = 0 + const topMax = doc.clientHeight - topMin + const [anchorX, anchorY] = parsePoint(anchorPoint) + const [dialogX, dialogY] = parsePoint(dialogPoint) + const margin = parseMargin(dialogMargin) + + // first we line of the top left to where we want it on the anchor. + // then we adjust the dialog to that point. + // then we make sure we don't overflow the window + switch (anchorX) { + case "center": + left = anchorRect.left + anchorRect.width / 2 + break + case "left": + left = left + 0 + break + case "right": + left = left + anchorRect.width + break + default: + break + } + + switch (anchorY) { + case "center": + top = anchorRect.top + anchorRect.height / 2 + break + case "top": + top = top + 0 + break + case "bottom": + top = top + anchorRect.height + break + } + + // Adjust dialog + switch (dialogX) { + case "center": + left = left - dialogRect.width / 2 + break + case "left": + left = left + 0 + margin.left + break + case "right": + left = left - dialogRect.width - margin.right + break + default: + // handle % and px heres + break + } + + switch (dialogY) { + case "center": + top = top - dialogRect.height / 2 + break + case "top": + top = top + 0 + margin.top + break + case "bottom": + top = top - dialogRect.height - margin.bottom + break + default: + // handle % and px here + break + } + + if (keepOnScreen) { + const {width, height} = dialogRect + if (left + width > leftMax) { + const diff = left + width - leftMax + left -= diff + } + // then If you overflow to the left, set at left limit + if (left < leftMin) { + left = leftMin + } + // If you overflow on the bottom, back up + if (top + height > topMax) { + const diff = top + height - topMax + top -= diff + } + // then If you overflow on the top, set at top limit + if (top < topMin) { + top = topMin + } + } + setPosition((s) => ({...s, left, top})) + } + + useLayoutEffect(() => { + run() + }, [ + dialog && dialog.open, + anchor, + anchorPoint, + dialogPoint, + dialogMargin, + props.isOpen, + keepOnScreen, + ]) + + useListener(global.window, "resize", run) + + return position +} diff --git a/src/components/icon-button.tsx b/src/components/icon-button.tsx index 1cbd5047d2..7f98631499 100644 --- a/src/components/icon-button.tsx +++ b/src/components/icon-button.tsx @@ -1,4 +1,9 @@ -import React, {MouseEvent, MouseEventHandler} from "react" +import React, { + MouseEvent, + MouseEventHandler, + MutableRefObject, + forwardRef, +} from "react" import {BoundCommand} from "src/app/commands/command" import Icon from "src/app/core/icon-temp" import {invoke} from "src/core/invoke" @@ -32,11 +37,12 @@ const BG = styled.button` } ` -export function IconButton( +export const IconButton = forwardRef(function IconButton( props: MenuItem & { className?: string onClick?: MouseEventHandler - } + }, + ref: MutableRefObject ) { function onClick(e: MouseEvent) { if (props.onClick) { @@ -51,6 +57,7 @@ export function IconButton( } return ( ) -} +}) diff --git a/src/core/correlations.ts b/src/core/correlations.ts index cb529dd3f0..fcf102ba0b 100644 --- a/src/core/correlations.ts +++ b/src/core/correlations.ts @@ -1,36 +1,8 @@ -import {Collector} from "@brimdata/zed-js" -import ErrorFactory from "src/js/models/ErrorFactory" -import Results from "src/js/state/Results" import {Thunk} from "src/js/state/types" import {invoke} from "./invoke" +import {firstPage} from "./query/run" export const runCorrelations = (): Thunk => async (dispatch, _) => { const correlations = await invoke("getCorrelationsOp") - correlations.forEach(({id, query}) => dispatch(runCorrelation(id, query))) + correlations.forEach(({id, query}) => dispatch(firstPage({id, query}))) } - -const runCorrelation = - (id: string, query: string): Thunk => - async (dispatch, getState, {api}) => { - const tabId = api.current.tabId - const key = api.current.location.key - const collect: Collector = (data) => { - dispatch(Results.setValues({id, tabId, values: data.rows})) - dispatch(Results.setShapes({id, tabId, shapes: data.shapesMap})) - } - - dispatch(Results.init({query, id, tabId, key})) - try { - const paginatedQuery = Results.getPaginatedQuery(id)(getState()) - const res = await api.query(paginatedQuery, {id, tabId, collect}) - await res.promise - dispatch(Results.success({id, tabId, count: res.rows.length})) - return res - } catch (e) { - if (e instanceof DOMException && e.message.match(/user aborted/)) return - console.log(e) - dispatch( - Results.error({id, tabId, error: ErrorFactory.create(e).message}) - ) - } - } diff --git a/src/core/field-path.ts b/src/core/field-path.ts new file mode 100644 index 0000000000..c4a6a697a1 --- /dev/null +++ b/src/core/field-path.ts @@ -0,0 +1,34 @@ +import * as zed from "@brimdata/zed-js" +import {arrayWrap} from "src/util/array-wrap" + +/** + * A field path represents the location of a field in a Zed record. + * A field path can be nested, reaching into nested records. That's + * why it's represented as an array of strings. Each item in the + * array is the name of a nested field. + */ +export class FieldPath { + locator: string[] + constructor(locator: string | string[] | zed.Field) { + this.locator = + locator instanceof zed.Field ? locator.path : arrayWrap(locator) + } + + toString() { + const result = [] + this.locator.forEach((path, i) => { + if (needsQuotes(path)) { + // if first path needs quoting, use 'this' as the bracket parent + if (i === 0) result.push("this") + result.push(`["${path}"]`) + } else { + // prepend path with '.' unless it is the first + if (i !== 0) result.push(".") + result.push(path) + } + }) + return result.join("") + } +} + +const needsQuotes = (fieldName: string) => !/^[a-zA-Z_$][\w]*$/.test(fieldName) diff --git a/src/core/menu/show-context-menu.ts b/src/core/menu/show-context-menu.ts index cf9342dd32..4fd0d09c92 100644 --- a/src/core/menu/show-context-menu.ts +++ b/src/core/menu/show-context-menu.ts @@ -36,7 +36,6 @@ function sanitizeMenuItem(item: any) { } function findItem(id: string, template: MenuItemConstructorOptions[]) { - console.log(id, template) for (let item of template) { if (item.id === id || item.label === id) return item if (item.submenu) { diff --git a/src/core/query/run.ts b/src/core/query/run.ts new file mode 100644 index 0000000000..ac65391bc7 --- /dev/null +++ b/src/core/query/run.ts @@ -0,0 +1,61 @@ +import {ResultStream} from "@brimdata/zed-js" +import ErrorFactory from "src/js/models/ErrorFactory" +import Current from "src/js/state/Current" +import Results from "src/js/state/Results" +import {Thunk} from "src/js/state/types" +import {isAbortError} from "src/util/is-abort-error" + +export function nextPage(id: string): Thunk { + return async (dispatch, getState) => { + if (Results.isFetching(id)(getState())) return + if (Results.isComplete(id)(getState())) return + if (Results.isLimited(id)(getState())) return + dispatch(Results.nextPage({id})) + dispatch(run(id)) + } +} + +export function firstPage(opts: {id: string; query: string}): Thunk { + return async (dispatch, getState, {api}) => { + const {id, query} = opts + const key = Current.getLocation(getState()).key + const tabId = api.current.tabId + dispatch(Results.init({query, key, id, tabId})) + dispatch(run(id)) + } +} + +function run(id: string): Thunk> { + return async (dispatch, getState, {api}) => { + const tabId = api.current.tabId + const isFirstPage = Results.getPage(id)(getState()) === 1 + const prevVals = Results.getValues(id)(getState()) + const prevShapes = Results.getShapes(id)(getState()) + const paginatedQuery = Results.getPaginatedQuery(id)(getState()) + + try { + const res = await api.query(paginatedQuery, { + id, + tabId, + }) + res.collect(({rows, shapesMap}) => { + const values = isFirstPage ? rows : [...prevVals, ...rows] + const shapes = isFirstPage ? shapesMap : {...prevShapes, ...shapesMap} + dispatch(Results.setValues({id, tabId, values})) + dispatch(Results.setShapes({id, tabId, shapes})) + }, {}) + await res.promise + dispatch(Results.success({id, tabId, count: res.rows.length})) + return res + } catch (e) { + if (isAbortError(e)) { + return null + } else { + dispatch( + Results.error({id, tabId, error: ErrorFactory.create(e).message}) + ) + } + return null + } + } +} diff --git a/src/core/query/use-query.ts b/src/core/query/use-query.ts new file mode 100644 index 0000000000..77e9325599 --- /dev/null +++ b/src/core/query/use-query.ts @@ -0,0 +1,13 @@ +import {useDispatch} from "src/app/core/state" +import {firstPage, nextPage} from "./run" +import {useCallback} from "react" + +export function useRun(opts: {id: string; query: string}) { + const dispatch = useDispatch() + return useCallback(() => dispatch(firstPage(opts)), [opts.id, opts.query]) +} + +export function useNextPage(id: string) { + const dispatch = useDispatch() + return useCallback(() => dispatch(nextPage(id)), [id]) +} diff --git a/src/core/query/use-results.ts b/src/core/query/use-results.ts new file mode 100644 index 0000000000..3339e7713f --- /dev/null +++ b/src/core/query/use-results.ts @@ -0,0 +1,10 @@ +import {useSelector} from "react-redux" +import Results from "src/js/state/Results" + +export function useResults(id: string) { + const data = useSelector(Results.getValues(id)) + const status = useSelector(Results.getStatus(id)) + const shapes = useSelector(Results.getShapes(id)) + const error = useSelector(Results.getError(id)) + return {data, status, shapes, error} +} diff --git a/src/core/state/create-crud-slice.ts b/src/core/state/create-crud-slice.ts new file mode 100644 index 0000000000..e0c5b00fb3 --- /dev/null +++ b/src/core/state/create-crud-slice.ts @@ -0,0 +1,62 @@ +import { + EntityAdapter, + EntityId, + EntityState, + PayloadAction, + Update, + createSlice, +} from "@reduxjs/toolkit" + +export function createCrudSlice(opts: { + name: string + adapter: EntityAdapter +}) { + const {name, adapter} = opts + + function getId(arg: T | EntityId) { + return typeof arg === "string" || typeof arg === "number" + ? arg + : adapter.selectId(arg) + } + + return createSlice({ + name, + initialState: {ids: [], entities: {}} as EntityState, + reducers: { + sync: (state, action: PayloadAction) => { + adapter.setAll(state as EntityState, action.payload) + }, + create: (state, action: PayloadAction) => { + if (Array.isArray(action.payload)) { + adapter.addMany(state as EntityState, action.payload) + } else { + adapter.addOne(state as EntityState, action.payload) + } + }, + update: (state, action: PayloadAction | Update[]>) => { + if (Array.isArray(action.payload)) { + adapter.updateMany(state as EntityState, action.payload) + } else { + adapter.updateOne(state as EntityState, action.payload) + } + }, + upsert: (state, action: PayloadAction) => { + if (Array.isArray(action.payload)) { + adapter.upsertMany(state as EntityState, action.payload) + } else { + adapter.upsertOne(state as EntityState, action.payload) + } + }, + delete: (state, action: PayloadAction) => { + if (Array.isArray(action.payload)) { + adapter.removeMany(state as EntityState, action.payload.map(getId)) + } else { + adapter.removeOne(state as EntityState, getId(action.payload)) + } + }, + deleteAll: (state) => { + adapter.removeAll(state as EntityState) + }, + }, + }) +} diff --git a/src/css/_zeek-plugin.scss b/src/css/_zeek-plugin.scss index adf11139c7..799b9ec74d 100644 --- a/src/css/_zeek-plugin.scss +++ b/src/css/_zeek-plugin.scss @@ -16,7 +16,7 @@ $unknown-type-color: #afafaf; @include zeek-bg-color("dhcp", #00578a); @include zeek-bg-color("dns", #1ca0f2); @include zeek-bg-color("ftp", #392277); -@include zeek-bg-color("http", hsl(49, 93%, 58%)); +@include zeek-bg-color("http", #f8d330); @include zeek-bg-color("files", #ad3f95); @include zeek-bg-color("mysql", #d28204); @include zeek-bg-color("irc", #00d1a6); @@ -24,7 +24,7 @@ $unknown-type-color: #afafaf; @include zeek-bg-color("kerberos", #fbf758); @include zeek-bg-color("sip", #006c7b); @include zeek-bg-color("smtp", #e2e317); -@include zeek-bg-color("ssl", hsl(0, 0%, 3%)); +@include zeek-bg-color("ssl", #080808); @include zeek-bg-color("ssh", #535765); @include zeek-bg-color("syslog", #ddb81d); @include zeek-bg-color("tunnel", #007249); @@ -39,10 +39,10 @@ $unknown-type-color: #afafaf; @include zeek-bg-color("dpd", #256453); @include zeek-bg-color("notice", red); @include zeek-bg-color("capture_loss", purple); -@include zeek-bg-color("software", hsl(203, 89%, 68%)); +@include zeek-bg-color("software", #65bef6); @include zeek-bg-color("stats", #5ec4a8); @include zeek-bg-color("known_hosts", #eeb457); -@include zeek-bg-color("known_services", darken(#eeb457, 10%)); +@include zeek-bg-color("known_services", #cc943a); @include zeek-bg-color("alert-1", var(--alert-1)); @include zeek-bg-color("alert-2", var(--alert-2)); @include zeek-bg-color("alert-3", var(--alert-3)); diff --git a/src/css/settings/_colors.scss b/src/css/settings/_colors.scss index 51811ce4af..0579874993 100644 --- a/src/css/settings/_colors.scss +++ b/src/css/settings/_colors.scss @@ -52,6 +52,7 @@ --primary-color-darker: hsl(212, 72%, 48%); --foreground-color: var(--aqua); + --foreground-color-light: #8b8f94; --button-background: hsl(212, 5%, 92%); --button-background-hover: hsl(212, 5%, 90%); diff --git a/src/domain/plugin-api.ts b/src/domain/plugin-api.ts index 5881109f8b..1b0eaafda2 100644 --- a/src/domain/plugin-api.ts +++ b/src/domain/plugin-api.ts @@ -3,6 +3,7 @@ import {CorrelationsApi} from "./correlations/plugin-api" import {EnvApi} from "./env/plugin-api" import {LoadersApi} from "./loaders/plugin-api" import {PanesApi} from "./panes/plugin-api" +import {PoolsApi} from "./pools/plugin-api" import {ResultsApi} from "./results/plugin-api" import {SessionApi} from "./session/plugin-api" import {WindowApi} from "./window/plugin-api" @@ -17,3 +18,4 @@ export const loaders = new LoadersApi() export const session = new SessionApi() export const correlations = new CorrelationsApi() export const configurations = new ConfigurationsApi() +export const pools = new PoolsApi() diff --git a/src/domain/pools/messages.ts b/src/domain/pools/messages.ts index fa633453fd..6e26828145 100644 --- a/src/domain/pools/messages.ts +++ b/src/domain/pools/messages.ts @@ -1,5 +1,7 @@ import {CreatePoolOpts, LoadOpts} from "@brimdata/zed-js" import {PoolUpdate} from "./types" +import {Update} from "@reduxjs/toolkit" +import {PoolSetting} from "src/js/state/PoolSettings/types" export type PoolsOperations = { "pools.create": ( @@ -10,4 +12,6 @@ export type PoolsOperations = { "pools.update": (lakeId: string, update: PoolUpdate | PoolUpdate[]) => void "pools.load": (poolId: string, data: string, opts: Partial) => void + "pools.updateSettings": (update: Update) => void + "pools.getSettings": (id: string) => PoolSetting | null } diff --git a/src/domain/pools/operations.ts b/src/domain/pools/operations.ts index aada07f3ac..73f7a2b936 100644 --- a/src/domain/pools/operations.ts +++ b/src/domain/pools/operations.ts @@ -1,7 +1,8 @@ import {createOperation} from "src/core/operations" import {CreatePoolOpts, LoadOpts} from "@brimdata/zed-js" -import {lake} from "src/zui" +import {lake, pools} from "src/zui" +import PoolSettings from "src/js/state/PoolSettings" export const create = createOperation( "pools.create", @@ -13,6 +14,7 @@ export const create = createOperation( ) => { const client = await main.createClient(lakeId) const {pool} = await client.createPool(name, opts) + pools.emit("create", {pool}) return pool.id as string } ) @@ -36,3 +38,19 @@ export const load = createOperation( }) } ) + +export const updateSettings = createOperation( + "pools.updateSettings", + async ({main}, update) => { + const dispatch = main.store.dispatch + dispatch(PoolSettings.create({id: update.id as string})) + dispatch(PoolSettings.update(update)) + } +) + +export const getSettings = createOperation( + "pools.getSettings", + ({main}, id) => { + return PoolSettings.find(main.store.getState(), id) + } +) diff --git a/src/domain/pools/plugin-api.ts b/src/domain/pools/plugin-api.ts new file mode 100644 index 0000000000..f99d5780ac --- /dev/null +++ b/src/domain/pools/plugin-api.ts @@ -0,0 +1,44 @@ +import {EventEmitter} from "events" +import {Pool} from "@brimdata/zed-js" +import {updateSettings} from "./operations" + +type Events = { + create: (event: {pool: Pool}) => void +} +export class PoolsApi { + private emitter = new EventEmitter() + + configure(poolId: string) { + return new PoolConfiguration(poolId) + } + + on(name: K, handler: Events[K]) { + this.emitter.on(name, handler) + } + + emit( + name: K, + ...args: Parameters + ) { + this.emitter.emit(name, ...args) + } + + _teardown() { + this.emitter.removeAllListeners() + } +} + +type ConfigMap = { + timeField: string + colorField: string + colorMap: Record +} + +class PoolConfiguration { + constructor(public id: string) {} + + set(key: K, value: ConfigMap[K]) { + updateSettings.run({id: this.id, changes: {[key]: value}}) + return this + } +} diff --git a/src/domain/results/utils/prep-export-query.ts b/src/domain/results/utils/prep-export-query.ts index c6e43fecee..9cd5a41e34 100644 --- a/src/domain/results/utils/prep-export-query.ts +++ b/src/domain/results/utils/prep-export-query.ts @@ -1,10 +1,10 @@ import ZuiApi from "src/js/api/zui-api" import program from "src/js/models/program" import Results from "src/js/state/Results" -import {MAIN_RESULTS} from "src/js/state/Results/types" +import {RESULTS_QUERY} from "src/panes/results-pane/run-results-query" export function prepExportQuery(api: ZuiApi, format: string) { - let query = Results.getQuery(MAIN_RESULTS)(api.getState()) + let query = Results.getQuery(RESULTS_QUERY)(api.getState()) query = cutColumns(query, api) query = maybeFuse(query, format) return query diff --git a/src/domain/session/plugin-api.ts b/src/domain/session/plugin-api.ts index 048c3f03ef..a536f150aa 100644 --- a/src/domain/session/plugin-api.ts +++ b/src/domain/session/plugin-api.ts @@ -30,4 +30,8 @@ export class SessionApi { goForward() { sendToFocusedWindow("session.goForward") } + + _teardown() { + this.emitter.removeAllListeners() + } } diff --git a/src/electron/run-main/run-plugins.ts b/src/electron/run-main/run-plugins.ts index 7406ca8370..9b73683184 100644 --- a/src/electron/run-main/run-plugins.ts +++ b/src/electron/run-main/run-plugins.ts @@ -5,7 +5,9 @@ import {createPluginContext} from "src/core/plugin" // main process code. import * as brimcap from "src/plugins/brimcap" +import * as corePool from "src/plugins/core-pool" export async function runPlugins() { + corePool.activate(createPluginContext("core-pool")) brimcap.activate(createPluginContext("brimcap")) } diff --git a/src/electron/start.test.ts b/src/electron/start.test.ts index 968c942ee3..35c8931d47 100644 --- a/src/electron/start.test.ts +++ b/src/electron/start.test.ts @@ -4,6 +4,7 @@ import {ZuiMain} from "./zui-main" import {installExtensions} from "./extensions" import {main} from "./run-main/run-main" import env from "src/app/core/env" +import {teardown} from "src/test/system/teardown" jest.mock("./extensions", () => ({ installExtensions: jest.fn(), @@ -11,6 +12,8 @@ jest.mock("./extensions", () => ({ jest.mock("@brimdata/zed-node") +afterEach(teardown) + test("start is called in zed lake", async () => { const appMain = (await main({devtools: false, autoUpdater: false})) as ZuiMain expect(appMain.lake.start).toHaveBeenCalledTimes(1) diff --git a/src/js/@types/d3.ts b/src/js/@types/d3.ts new file mode 100644 index 0000000000..7982bf310a --- /dev/null +++ b/src/js/@types/d3.ts @@ -0,0 +1,5 @@ +import {ContainerElement} from "d3" + +declare module "d3" { + function pointer(event: PointerEvent, el?: ContainerElement): [number, number] +} diff --git a/src/js/api/core/query.ts b/src/js/api/core/query.ts deleted file mode 100644 index ca9e59c3ad..0000000000 --- a/src/js/api/core/query.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {Collector, ResultStream} from "@brimdata/zed-js" -import {Thunk} from "src/js/state/types" -import ZuiApi from "../zui-api" - -export type QueryOptions = { - id?: string - tabId?: string - collect?: Collector -} - -export function query( - body: string, - opts: QueryOptions = {} -): Thunk> { - return async (d, gs, {api}) => { - const zealot = await api.getZealot() - const [signal, cleanup] = createAbortable(api, opts.tabId, opts.id) - let res: ResultStream - try { - res = await zealot.query(body, {signal}) - if (opts.collect) await res.collect(opts.collect) - } finally { - cleanup() - } - return res - } -} - -function createAbortable(api: ZuiApi, tab?: string, tag?: string) { - api.abortables.abort({tab, tag}) - const ctl = new AbortController() - const id = api.abortables.add({abort: () => ctl.abort(), tab, tag}) - const cleanup = () => api.abortables.remove(id) - return [ctl.signal, cleanup] as const -} diff --git a/src/js/api/zui-api.ts b/src/js/api/zui-api.ts index 61957f3722..1392962410 100644 --- a/src/js/api/zui-api.ts +++ b/src/js/api/zui-api.ts @@ -1,14 +1,13 @@ import {Abortables} from "src/app/core/models/abortables" import toast from "react-hot-toast" import {getZealot} from "./core/get-zealot" -import {AppDispatch, GetState} from "../state/types" +import {AppDispatch, GetState, State} from "../state/types" import {QueriesApi} from "./queries/queries-api" import {PoolsApi} from "./pools/pools-api" import {CommandsApi} from "./commands/cmmands-api" import {Detail, MenusApi, Search} from "./menus/menus-api" import {ConfigurationsApi} from "./configurations/configurations-api" import {ToolbarsApi} from "./toolbars/toolbars-api" -import {query, QueryOptions} from "./core/query" import {CurrentApi} from "./current/current-api" import {EditorApi} from "./editor/editor-api" import {NoticeApi} from "./notice/notice-api" @@ -57,7 +56,35 @@ export default class ZuiApi { return this.dispatch(getZealot(lake)) } - query(body: string, opts: QueryOptions = {}) { - return this.dispatch(query(body, opts)) + createAbortable(tab?: string, tag?: string) { + this.abortables.abort({tab, tag}) + const ctl = new AbortController() + const id = this.abortables.add({ + abort: () => { + console.log("aborted", tab, tag) + ctl.abort() + }, + tab, + tag, + }) + const cleanup = () => this.abortables.remove(id) + return [ctl.signal, cleanup] as const + } + + async query(body: string, opts: {id?: string; tabId?: string} = {}) { + const zealot = await this.getZealot() + const [signal, cleanup] = this.createAbortable(opts.tabId, opts.id) + try { + const resp = await zealot.query(body, {signal}) + resp.promise.finally(cleanup) + return resp + } catch (e) { + cleanup() + throw e + } + } + + select ReturnType>(fn: T) { + return fn(this.getState()) } } diff --git a/src/js/components/EmptyMessage.tsx b/src/js/components/EmptyMessage.tsx index 4203a0f0ba..ee7548ba1d 100644 --- a/src/js/components/EmptyMessage.tsx +++ b/src/js/components/EmptyMessage.tsx @@ -2,9 +2,6 @@ import React from "react" import classNames from "classnames" export default function EmptyMessage({show}: {show: boolean}) { - return ( -

- No Chart Data -

- ) + if (!show) return null + return

No Chart Data

} diff --git a/src/js/components/HistogramTooltip.tsx b/src/js/components/HistogramTooltip.tsx index 84d8d14815..2a69784c72 100644 --- a/src/js/components/HistogramTooltip.tsx +++ b/src/js/components/HistogramTooltip.tsx @@ -6,16 +6,22 @@ import * as fmt from "../lib/fmt" type Props = { ts: Date segments: [string, number][] + chart: any } -const HistogramTooltip = ({segments, ts}: Props) => { +const HistogramTooltip = ({chart, segments, ts}: Props) => { const total = segments.reduce((sum, [_, count]) => (sum += count), 0) const rows = segments .sort((a, b) => b[1] - a[1]) .map(([path, count]) => ( - {path} + + {path.substring(0, 30)} + {fmt.withCommas(count)} diff --git a/src/js/components/common/forms/FileInput.tsx b/src/js/components/common/forms/FileInput.tsx index 0fbc556d24..c7cbb3452a 100644 --- a/src/js/components/common/forms/FileInput.tsx +++ b/src/js/components/common/forms/FileInput.tsx @@ -23,7 +23,7 @@ const Input = styled(TextInput)` ` export default function FileInput(props: Props) { - const [picker, ref] = useCallbackRef() + const [picker, ref] = useCallbackRef() const [bindDropzone, dragging] = useDropzone(onDrop) const [value, setValue] = useState(props.defaultValue) diff --git a/src/js/components/status-bar/query-progress.tsx b/src/js/components/status-bar/query-progress.tsx index f39b1704c9..7e32591735 100644 --- a/src/js/components/status-bar/query-progress.tsx +++ b/src/js/components/status-bar/query-progress.tsx @@ -1,7 +1,7 @@ import React from "react" import {useSelector} from "react-redux" import Results from "src/js/state/Results" -import {MAIN_RESULTS} from "src/js/state/Results/types" +import {RESULTS_QUERY} from "src/panes/results-pane/run-results-query" import styled from "styled-components" const Loader = styled.div` @@ -50,8 +50,8 @@ const Span = styled.span` ` export function QueryProgress() { - const status = useSelector(Results.getStatus(MAIN_RESULTS)) - const count = useSelector(Results.getCount(MAIN_RESULTS)) + const status = useSelector(Results.getStatus(RESULTS_QUERY)) + const count = useSelector(Results.getCount(RESULTS_QUERY)) if (status === "FETCHING") { return ( diff --git a/src/js/components/status-bar/type-count.tsx b/src/js/components/status-bar/type-count.tsx index e15da9b052..bd4fcdeae7 100644 --- a/src/js/components/status-bar/type-count.tsx +++ b/src/js/components/status-bar/type-count.tsx @@ -1,11 +1,11 @@ import React from "react" import {useSelector} from "react-redux" import Results from "src/js/state/Results" -import {MAIN_RESULTS} from "src/js/state/Results/types" +import {RESULTS_QUERY} from "src/panes/results-pane/run-results-query" export function TypeCount() { - const shapes = useSelector(Results.getShapes(MAIN_RESULTS)) - const status = useSelector(Results.getStatus(MAIN_RESULTS)) + const shapes = useSelector(Results.getShapes(RESULTS_QUERY)) + const status = useSelector(Results.getStatus(RESULTS_QUERY)) if (["COMPLETE", "LIMIT", "INCOMPLETE"].includes(status)) { return Shapes: {Object.keys(shapes).length} } else { diff --git a/src/js/flows/inspectSearch.ts b/src/js/flows/inspectSearch.ts index 429912ded1..48db98c605 100644 --- a/src/js/flows/inspectSearch.ts +++ b/src/js/flows/inspectSearch.ts @@ -1,5 +1,5 @@ import Results from "../state/Results" -import {MAIN_RESULTS} from "../state/Results/types" +import {RESULTS_QUERY} from "src/panes/results-pane/run-results-query" import {Thunk} from "../state/types" export const inspectSearch = @@ -7,7 +7,7 @@ export const inspectSearch = async (dispatch, getState, {api}) => { const zealot = await api.getZealot() - return zealot.curl(Results.getQuery(MAIN_RESULTS)(getState()), { + return zealot.curl(Results.getQuery(RESULTS_QUERY)(getState()), { format: "zson", }) } diff --git a/src/js/initializers/initDOM.ts b/src/js/initializers/initDOM.ts index a6d8d18d37..716ddf5156 100644 --- a/src/js/initializers/initDOM.ts +++ b/src/js/initializers/initDOM.ts @@ -1,4 +1,4 @@ -export default function () { +export default function initDOM() { appendDivId("notification-root") appendDivId("modal-root") appendDivId("tooltip-root") diff --git a/src/js/lib/TimeWindow.ts b/src/js/lib/TimeWindow.ts index a72cbe7e53..7cb5688f7d 100644 --- a/src/js/lib/TimeWindow.ts +++ b/src/js/lib/TimeWindow.ts @@ -1,8 +1,6 @@ import isEqual from "lodash/isEqual" import moment from "moment" -import time from "../models/time" - -import {TimeUnit} from "./" +import time, {TimeUnit} from "../models/time" export type DateTuple = [Date, Date] diff --git a/src/js/lib/histogramInterval.test.ts b/src/js/lib/histogramInterval.test.ts deleted file mode 100644 index e14374ddcb..0000000000 --- a/src/js/lib/histogramInterval.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import time from "../models/time" -import histogramInterval from "./histogramInterval" -import {DateTuple} from "./TimeWindow" - -const start = new Date() - -test("returns the proper format", () => { - const end = time(start).add(5, "minutes").toDate() - const timeWindow: DateTuple = [start, end] - - expect(histogramInterval(timeWindow)).toEqual({ - number: 1, - unit: "second", - roundingUnit: "second", - }) -}) diff --git a/src/js/lib/histogramInterval.ts b/src/js/lib/histogramInterval.ts deleted file mode 100644 index c1ab6ee952..0000000000 --- a/src/js/lib/histogramInterval.ts +++ /dev/null @@ -1,79 +0,0 @@ -import moment from "moment" -import {TimeUnit} from "." - -import {DateTuple} from "../lib/TimeWindow" - -export type Interval = { - number: number - unit: LongTimeUnit - roundingUnit: TimeUnit -} - -export type LongTimeUnit = - | "millisecond" - | "second" - | "minute" - | "hour" - | "day" - | "month" - -export const timeUnits = { - millisecond: "ms", - second: "s", - minute: "m", - hour: "h", - day: "d", - week: "w", - year: "y", -} - -export default function histogramInterval([from, to]: DateTuple): Interval { - const duration = moment.duration(moment(to).diff(moment(from))) - - if (duration.asMinutes() <= 1) - return {number: 100, unit: "millisecond", roundingUnit: "second"} - - if (duration.asMinutes() <= 3) - return {number: 500, unit: "millisecond", roundingUnit: "second"} - - if (duration.asMinutes() <= 5) - return {number: 1, unit: "second", roundingUnit: "second"} - - if (duration.asMinutes() <= 10) - return {number: 10, unit: "second", roundingUnit: "second"} - - if (duration.asMinutes() <= 20) - return {number: 20, unit: "second", roundingUnit: "second"} - - if (duration.asMinutes() <= 30) - return {number: 30, unit: "second", roundingUnit: "minute"} - - if (duration.asHours() <= 2) - return {number: 1, unit: "minute", roundingUnit: "minute"} - - if (duration.asHours() <= 4) - return {number: 5, unit: "minute", roundingUnit: "hour"} - - if (duration.asHours() <= 12) - return {number: 15, unit: "minute", roundingUnit: "hour"} - - if (duration.asDays() <= 1) - return {number: 30, unit: "minute", roundingUnit: "hour"} - - if (duration.asDays() <= 3) - return {number: 1, unit: "hour", roundingUnit: "hour"} - - if (duration.asDays() <= 14) - return {number: 6, unit: "hour", roundingUnit: "day"} - - if (duration.asDays() <= 60) - return {number: 12, unit: "hour", roundingUnit: "day"} - - if (duration.asDays() <= 120) - return {number: 1, unit: "day", roundingUnit: "day"} - - if (duration.asMonths() <= 12) - return {number: 7, unit: "day", roundingUnit: "day"} - - return {number: 30, unit: "day", roundingUnit: "day"} -} diff --git a/src/js/lib/index.ts b/src/js/lib/index.ts index 440059b79f..96cd7170c8 100644 --- a/src/js/lib/index.ts +++ b/src/js/lib/index.ts @@ -37,33 +37,3 @@ export default { }, sleep: (ms: number) => new Promise((r) => setTimeout(r, ms)), } - -export type TimeUnit = - | "years" - | "year" - | "y" - | "months" - | "month" - | "M" - | "weeks" - | "week" - | "w" - | "days" - | "day" - | "d" - | "hours" - | "hour" - | "h" - | "minutes" - | "minute" - | "m" - | "seconds" - | "second" - | "s" - | "milliseconds" - | "millisecond" - | "ms" - -export type TimeObj = {minutes: number; hours: number} - -export type EpochObj = {sec: number; ns: number} diff --git a/src/js/models/time.ts b/src/js/models/time.ts index d25017371b..0847842b18 100644 --- a/src/js/models/time.ts +++ b/src/js/models/time.ts @@ -1,11 +1,39 @@ import {isString} from "lodash" import moment from "moment-timezone" -import {TimeUnit} from "../lib" import {isBigInt, isDate} from "../lib/is" import {DateTuple} from "../lib/TimeWindow" import relTime from "./relTime" import {Span, Ts} from "./span" +export type TimeUnit = + | "years" + | "year" + | "y" + | "months" + | "month" + | "M" + | "weeks" + | "week" + | "w" + | "days" + | "day" + | "d" + | "hours" + | "hour" + | "h" + | "minutes" + | "minute" + | "m" + | "seconds" + | "second" + | "s" + | "milliseconds" + | "millisecond" + | "ms" + +export type TimeObj = {minutes: number; hours: number} + +export type EpochObj = {sec: number; ns: number} function time(val: Ts | bigint | Date | string = new Date()) { let ts: Ts if (isBigInt(val)) { diff --git a/src/js/state/Current/selectors.ts b/src/js/state/Current/selectors.ts index 7b5597dada..9d7dd2efbb 100644 --- a/src/js/state/Current/selectors.ts +++ b/src/js/state/Current/selectors.ts @@ -89,7 +89,6 @@ export const getActiveQuery = createSelector( getNamedQuery, getVersion, (session, query, version) => { - // diff(session) return new ActiveQuery(session, query, version || QueryVersions.initial()) } ) @@ -176,3 +175,15 @@ export function getOpEventContext(state: State) { } export type OpEventContext = ReturnType + +export const getPoolNameFromQuery = createSelector(getActiveQuery, (q) => { + return q.toAst().poolName +}) + +export const getPoolFromQuery = createSelector( + getPoolNameFromQuery, + getPools, + (name, pools) => { + return pools.find((p) => p.data.name === name) ?? null + } +) diff --git a/src/js/state/Editor/reducer.ts b/src/js/state/Editor/reducer.ts index 37df8e56e8..ee46a6cdd9 100644 --- a/src/js/state/Editor/reducer.ts +++ b/src/js/state/Editor/reducer.ts @@ -9,12 +9,8 @@ const slice = createSlice({ pins: [] as QueryPin[], pinEditIndex: null as null | number, pinHoverIndex: null as null | number, - height: 96, }, reducers: { - setHeight(s, a: PayloadAction) { - s.height = a.payload - }, setValue(s, a: PayloadAction) { s.value = a.payload }, diff --git a/src/js/state/Editor/selectors.ts b/src/js/state/Editor/selectors.ts index eda2bd6a1a..603263e8c6 100644 --- a/src/js/state/Editor/selectors.ts +++ b/src/js/state/Editor/selectors.ts @@ -10,10 +10,6 @@ export const getPinEditIndex = activeTabSelect((tab) => { return tab.editor.pinEditIndex }) -export const getHeight = activeTabSelect((tab) => { - return tab.editor.height -}) - export const getValue = activeTabSelect((tab) => { return tab.editor.value }) diff --git a/src/js/state/Histogram/build-query.ts b/src/js/state/Histogram/build-query.ts deleted file mode 100644 index 3849fb3f03..0000000000 --- a/src/js/state/Histogram/build-query.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {QueryModel} from "src/js/models/query-model" -import histogramInterval, {timeUnits} from "src/js/lib/histogramInterval" -import {DateTuple} from "src/js/lib/TimeWindow" -import Current from "src/js/state/Current" -import {TimeRangeQueryPin} from "src/js/state/Editor/types" -import {QueryVersion} from "src/js/state/QueryVersions/types" -import {Thunk} from "src/js/state/types" -import Pools from "../Pools" -import {actions} from "./reducer" - -export const buildHistogramQuery = - (): Thunk> => - async (dispatch, getState, {api}) => { - const poolName = api.current.poolName - const version = Current.getVersion(getState()) - const range = await dispatch(getRange(poolName)) - // this doesn't belong here - dispatch(actions.setRange(range)) - return histogramZed(QueryModel.versionToZed(version), range) - } - -export const getRange = - (name: string): Thunk | DateTuple> => - (dispatch) => { - const queryRange = dispatch(getRangeFromQuery()) - if (queryRange) return queryRange - else return dispatch(Pools.getTimeRange(name)) - } - -function histogramZed(baseQuery: string, range: DateTuple | null) { - if (!range) return null - const {number, unit} = histogramInterval(range) - const interval = `${number}${timeUnits[unit]}` - return `${baseQuery} | count() by every(${interval}), _path` -} - -const getRangeFromQuery = (): Thunk => (_, getState) => { - const snapshot = Current.getVersion(getState()) - return getCurrentRange(snapshot) -} - -const getCurrentRange = (snapshot: QueryVersion): DateTuple | null => { - const rangePin = snapshot.pins.find( - (p) => p.type === "time-range" && !p.disabled - ) as TimeRangeQueryPin - - if (rangePin) { - return [new Date(rangePin.from), new Date(rangePin.to)] - } else { - return null - } -} diff --git a/src/js/state/Histogram/reducer.ts b/src/js/state/Histogram/reducer.ts index 386da7f034..9d70be87f3 100644 --- a/src/js/state/Histogram/reducer.ts +++ b/src/js/state/Histogram/reducer.ts @@ -1,17 +1,32 @@ import {createSlice, PayloadAction} from "@reduxjs/toolkit" import {DateTuple} from "src/js/lib/TimeWindow" +import {Interval} from "src/panes/histogram-pane/get-interval" const slice = createSlice({ name: "TAB_HISTOGRAM", initialState: { - x: "ts", - by: "_path", + interval: null as null | Interval, range: null as null | DateTuple, + nullXCount: 0, + missingXCount: 0, }, reducers: { + init(s) { + s.nullXCount = 0 + s.missingXCount = 0 + }, setRange(s, a: PayloadAction) { s.range = a.payload }, + setInterval(s, a: PayloadAction) { + s.interval = a.payload + }, + setNullXCount(s, a: PayloadAction) { + s.nullXCount = a.payload + }, + setMissingXCount(s, a: PayloadAction) { + s.missingXCount = a.payload + }, }, }) diff --git a/src/js/state/Histogram/run-query.ts b/src/js/state/Histogram/run-query.ts deleted file mode 100644 index dd8b500589..0000000000 --- a/src/js/state/Histogram/run-query.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {Collector} from "@brimdata/zed-js" -import Results from "src/js/state/Results" -import {Thunk} from "src/js/state/types" -import {buildHistogramQuery} from "./build-query" - -export const HISTOGRAM_RESULTS = "zui/histogram" -const id = HISTOGRAM_RESULTS - -// This is looking very similar to Results/flows.ts fetchResults() -// Maybe this can be part of the api. It automatically saves it to -// the results reducer, and it paginates the query for you? -export const runHistogramQuery = - (): Thunk => - async (dispatch, getState, {api}) => { - const tabId = api.current.tabId - const key = api.current.location.key - dispatch(Results.init({id, tabId, query: "", key})) - const query = await dispatch(buildHistogramQuery()) - if (!query) return - dispatch(Results.init({id, tabId, query, key})) - const collect: Collector = ({rows, shapesMap}) => { - dispatch(Results.setValues({id, tabId, values: rows})) - dispatch(Results.setShapes({id, tabId, shapes: shapesMap})) - } - - try { - const res = await api.query(query, {tabId, id, collect}) - await res.promise - dispatch(Results.success({id, tabId, count: res.rows.length})) - } catch (error) { - if ( - error instanceof DOMException && - error.message.match(/user aborted/) - ) { - return - } - dispatch(Results.error({id, error, tabId})) - } - } diff --git a/src/js/state/Histogram/selectors.ts b/src/js/state/Histogram/selectors.ts index 6243557b87..74aad8ce6b 100644 --- a/src/js/state/Histogram/selectors.ts +++ b/src/js/state/Histogram/selectors.ts @@ -1,3 +1,20 @@ +import {HISTOGRAM_RESULTS} from "src/panes/histogram-pane/run-query" +import Results from "../Results" import activeTabSelect from "../Tab/activeTabSelect" +import {State} from "../types" export const getRange = activeTabSelect((t) => t.histogram.range) + +export const getInterval = activeTabSelect((t) => t.histogram.interval) + +export const getNullXCount = activeTabSelect((t) => t.histogram.nullXCount) + +export const getMissingXCount = activeTabSelect( + (t) => t.histogram.missingXCount +) + +export const getData = (state: State) => + Results.getValues(HISTOGRAM_RESULTS)(state) + +export const getError = (state: State) => + Results.getError(HISTOGRAM_RESULTS)(state) diff --git a/src/js/state/Layout/reducer.ts b/src/js/state/Layout/reducer.ts index a9f13d58b3..fc4ffefe16 100644 --- a/src/js/state/Layout/reducer.ts +++ b/src/js/state/Layout/reducer.ts @@ -1,7 +1,7 @@ import {createSlice, PayloadAction} from "@reduxjs/toolkit" import {ColumnHeadersViewState, ResultsView, PaneName} from "./types" -// This is tab level appearance stuff +// This is tab level ui persistence const slice = createSlice({ name: "TAB_LAYOUT", initialState: { @@ -13,6 +13,7 @@ const slice = createSlice({ isEditingTitle: false, titleFormAction: "create" as "create" | "update", showHistogram: true, + queryPanels: "", }, reducers: { showDetailPane: (s) => { @@ -50,6 +51,9 @@ const slice = createSlice({ toggleHistogram(s) { s.showHistogram = !s.showHistogram }, + setQueryPanels(s, value: PayloadAction) { + s.queryPanels = value.payload + }, }, }) diff --git a/src/js/state/Layout/selectors.ts b/src/js/state/Layout/selectors.ts index e19ef5713c..c1050b5b64 100644 --- a/src/js/state/Layout/selectors.ts +++ b/src/js/state/Layout/selectors.ts @@ -1,13 +1,13 @@ import {createSelector} from "@reduxjs/toolkit" import activeTabSelect from "../Tab/activeTabSelect" -import {MAIN_RESULTS} from "../Results/types" +import {RESULTS_QUERY} from "src/panes/results-pane/run-results-query" import {getShapes} from "../Results/selectors" const getResultsView = activeTabSelect((s) => s.layout.resultsView) const getEffectiveResultsView = createSelector( getResultsView, - getShapes(MAIN_RESULTS), + getShapes(RESULTS_QUERY), (view, shapes) => { const isSingleShape = Object.values(shapes).length === 1 if (isSingleShape) return view @@ -29,4 +29,5 @@ export default { getIsEditingTitle: activeTabSelect((s) => s.layout.isEditingTitle), getTitleFormAction: activeTabSelect((s) => s.layout.titleFormAction), getShowHistogram: activeTabSelect((s) => s.layout.showHistogram ?? true), + getQueryPanels: activeTabSelect((s) => s.layout.queryPanels), } diff --git a/src/js/state/PoolSettings/index.ts b/src/js/state/PoolSettings/index.ts new file mode 100644 index 0000000000..6c660ed808 --- /dev/null +++ b/src/js/state/PoolSettings/index.ts @@ -0,0 +1,8 @@ +import {actions, reducer} from "./reducer" +import * as selectors from "./selectors" + +export default { + ...actions, + ...selectors, + reducer, +} diff --git a/src/js/state/PoolSettings/reducer.ts b/src/js/state/PoolSettings/reducer.ts new file mode 100644 index 0000000000..4b663d3abe --- /dev/null +++ b/src/js/state/PoolSettings/reducer.ts @@ -0,0 +1,9 @@ +import {createEntityAdapter} from "@reduxjs/toolkit" +import {createCrudSlice} from "src/core/state/create-crud-slice" +import {PoolSetting} from "./types" + +const adapter = createEntityAdapter() +const slice = createCrudSlice({name: "$POOL_SETTINGS", adapter}) + +export const reducer = slice.reducer +export const actions = slice.actions diff --git a/src/js/state/PoolSettings/selectors.ts b/src/js/state/PoolSettings/selectors.ts new file mode 100644 index 0000000000..1eb1673b5c --- /dev/null +++ b/src/js/state/PoolSettings/selectors.ts @@ -0,0 +1,24 @@ +import {createSelector} from "@reduxjs/toolkit" +import {State} from "../types" + +export function getDefaults() { + return { + timeField: "ts", + colorField: "typeof(this)", + colorMap: null as Record | null, + } +} + +export function getEntities(state: State) { + return state.poolSettings.entities +} + +export const find = (state: State, id: string) => { + return state.poolSettings.entities[id] +} + +export const findWithDefaults = createSelector( + (_: State, id: string) => id, + getEntities, + (id, entities) => entities[id] ?? getDefaults() +) diff --git a/src/js/state/PoolSettings/types.ts b/src/js/state/PoolSettings/types.ts new file mode 100644 index 0000000000..e31cff0eec --- /dev/null +++ b/src/js/state/PoolSettings/types.ts @@ -0,0 +1,10 @@ +import {reducer} from "./reducer" + +export type PoolSetting = { + id: string + timeField?: string + colorField?: string + colorMap?: Record +} + +export type PoolSettingsState = ReturnType diff --git a/src/js/state/Pools/selectors.ts b/src/js/state/Pools/selectors.ts index 44d165416b..721ebbc7dd 100644 --- a/src/js/state/Pools/selectors.ts +++ b/src/js/state/Pools/selectors.ts @@ -26,7 +26,7 @@ export const getWarnings = export const raw = (state: State): PoolsState => state.pools export const all = createSelector( - (_, lakeId: string) => lakeId, + (_: State, lakeId: string) => lakeId, raw, (lakeId, pools) => { return Object.keys(pools[lakeId]) diff --git a/src/js/state/Queries/queries.test.ts b/src/js/state/Queries/queries.test.ts index aeeb98bc70..ee66fc25d7 100644 --- a/src/js/state/Queries/queries.test.ts +++ b/src/js/state/Queries/queries.test.ts @@ -10,6 +10,7 @@ import {Store} from "../types" let store: Store beforeEach(async () => { + jest.spyOn(console, "error").mockImplementation(() => {}) store = await initTestStore() }) diff --git a/src/js/state/Results/flows.ts b/src/js/state/Results/flows.ts deleted file mode 100644 index 2d3ed429fb..0000000000 --- a/src/js/state/Results/flows.ts +++ /dev/null @@ -1,54 +0,0 @@ -import ErrorFactory from "src/js/models/ErrorFactory" -import * as selectors from "./selectors" -import {Thunk} from "../types" -import {actions} from "./reducer" -import Current from "../Current" -import {MAIN_RESULTS} from "./types" - -const id = MAIN_RESULTS - -export function fetchFirstPage(query: string): Thunk { - return async (dispatch, getState) => { - const tabId = Current.getTabId(getState()) - const key = Current.getLocation(getState()).key - - dispatch(actions.init({query, key, id, tabId})) - dispatch(fetchResults(tabId)) - } -} - -export function fetchNextPage(): Thunk { - return async (dispatch, getState) => { - const tabId = Current.getTabId(getState()) - - dispatch(actions.nextPage({id: MAIN_RESULTS, tabId})) - dispatch(fetchResults(tabId)) - } -} - -function fetchResults(tabId: string): Thunk { - return async (dispatch, getState, {api}) => { - const prevVals = selectors.getValues(id)(getState()) - const prevShapes = selectors.getShapes(id)(getState()) - const collect = ({rows, shapesMap}) => { - const values = [...prevVals, ...rows] - const shapes = {...prevShapes, ...shapesMap} - dispatch(actions.setValues({id, tabId, values})) - dispatch(actions.setShapes({id, tabId, shapes})) - } - - try { - const res = await api.query(selectors.getPaginatedQuery(id)(getState()), { - id, - tabId, - collect, - }) - dispatch(actions.success({id, tabId, count: res.rows.length})) - } catch (e) { - if (e instanceof DOMException && e.message.match(/user aborted/)) return - dispatch( - actions.error({id, tabId, error: ErrorFactory.create(e).message}) - ) - } - } -} diff --git a/src/js/state/Results/index.ts b/src/js/state/Results/index.ts index be2ce3d68a..e0ae71850e 100644 --- a/src/js/state/Results/index.ts +++ b/src/js/state/Results/index.ts @@ -1,9 +1,7 @@ -import * as flows from "./flows" import {actions, reducer} from "./reducer" import * as selectors from "./selectors" export default { ...actions, ...selectors, - ...flows, reducer, } diff --git a/src/js/state/Results/reducer.ts b/src/js/state/Results/reducer.ts index 77e4279ab4..4e6f1bfe30 100644 --- a/src/js/state/Results/reducer.ts +++ b/src/js/state/Results/reducer.ts @@ -23,14 +23,13 @@ const slice = createSlice({ r.query = a.payload.query r.aggregation = program(a.payload.query).hasAnalytics() r.key = a.payload.key - r.values = [] - r.shapes = {} r.page = 1 r.status = "FETCHING" - r.error = null + r.values = [] + r.shapes = {} }, - nextPage(s, a: Pay<{id: string; tabId: string}>) { + nextPage(s, a: Pay<{id: string}>) { const r = access(s, a.payload.id) r.page += 1 r.status = "FETCHING" @@ -49,7 +48,7 @@ const slice = createSlice({ r.shapes = a.payload.shapes }, - success(s, a: Pay<{id: string; count: number; tabId: string}>) { + success(s, a: Pay<{id: string; count?: number; tabId: string}>) { const r = access(s, a.payload.id) if (r.aggregation && a.payload.count === r.aggregationLimit) { r.status = "LIMIT" @@ -61,7 +60,7 @@ const slice = createSlice({ r.error = null }, - error: (s, a: Pay<{id: string; error: any; tabId: string}>) => { + error: (s, a: Pay<{id: string; error: any; tabId?: string}>) => { const r = access(s, a.payload.id) r.error = a.payload.error r.status = "ERROR" diff --git a/src/js/state/Results/selectors.ts b/src/js/state/Results/selectors.ts index 7ee3246e61..7a917245e1 100644 --- a/src/js/state/Results/selectors.ts +++ b/src/js/state/Results/selectors.ts @@ -2,7 +2,6 @@ import {initialResultData} from "./util" import activeTabSelect from "../Tab/activeTabSelect" import {paginate} from "./paginate" import {ResultData, ResultsState} from "./types" -import {MAIN_RESULTS} from "./types" const initial = Object.freeze(initialResultData()) @@ -58,6 +57,8 @@ export const isIncomplete = resultsSelect( (results) => results.status === "INCOMPLETE" ) +export const isAggregation = resultsSelect((results) => results.aggregation) + export const getKey = resultsSelect((results) => { return results.key }) @@ -70,5 +71,3 @@ export const getError = resultsSelect((results) => results.error) export const getPage = resultsSelect((results) => results.page) export const getPerPage = resultsSelect((results) => results.perPage) export const getCount = resultsSelect((results) => results.values.length) - -export const getMainValues = getValues(MAIN_RESULTS) diff --git a/src/js/state/Results/types.ts b/src/js/state/Results/types.ts index 691a98593c..80b8e0e816 100644 --- a/src/js/state/Results/types.ts +++ b/src/js/state/Results/types.ts @@ -10,5 +10,3 @@ export type ResultsStatus = export type ResultData = ReturnType export type ResultsState = {[id: string]: ResultData} - -export const MAIN_RESULTS = "zui-results/main" diff --git a/src/js/state/Tab/activeTabSelect.ts b/src/js/state/Tab/activeTabSelect.ts index 9b8ee004d2..1eab6e1c6a 100644 --- a/src/js/state/Tab/activeTabSelect.ts +++ b/src/js/state/Tab/activeTabSelect.ts @@ -1,10 +1,9 @@ import {createSelector} from "reselect" -import {TabsState} from "../Tabs/types" import {State} from "../types" import {TabState} from "./types" -const getActiveTab = createSelector( - (state) => state.tabs, +const getActiveTab = createSelector( + (state: State) => state.tabs, (tabs) => { const tab = tabs.data.find((t) => t.id === tabs.active) if (!tab) throw new Error("Can't find active tab") @@ -15,9 +14,5 @@ const getActiveTab = createSelector( export default function activeTabSelect( selector: (tabState: TabState, state: State) => T ): (state: State) => T { - return createSelector( - getActiveTab, - (state) => state, - selector - ) + return createSelector(getActiveTab, (state) => state, selector) } diff --git a/src/js/state/Tabs/selectors.ts b/src/js/state/Tabs/selectors.ts index cee6ec47fe..7559554b55 100644 --- a/src/js/state/Tabs/selectors.ts +++ b/src/js/state/Tabs/selectors.ts @@ -1,8 +1,6 @@ import {createSelector} from "reselect" -import {TabState} from "../Tab/types" import {State} from "../types" import {createIsEqualSelector} from "../utils" -import {TabsState} from "./types" import {findQuerySessionTab} from "./find" export const getData = (state: State) => state.tabs.data @@ -10,8 +8,8 @@ export const getActive = (state: State) => state.tabs.active export const getCount = (state: State) => state.tabs.data.length export const getPreview = (state: State) => state.tabs.preview -export const getActiveTab = createSelector( - (state) => state.tabs, +export const getActiveTab = createSelector( + (state: State) => state.tabs, (tabs) => { const tab = tabs.data.find((t) => t.id === tabs.active) if (!tab) throw new Error("Can't find active tab") @@ -28,11 +26,10 @@ export const getIds = createIsEqualSelector( (ids) => ids ) -export const findFirstQuerySession = createSelector< - State, - TabState[], - TabState ->((state) => state.tabs.data, findQuerySessionTab) +export const findFirstQuerySession = createSelector( + (state: State) => state.tabs.data, + findQuerySessionTab +) export const findById = (tabId: string) => createSelector(getData, (tabs) => tabs.find((t) => t.id === tabId)) diff --git a/src/js/state/stores/get-persistable.ts b/src/js/state/stores/get-persistable.ts index 5bca7a00a4..ca35f6bc61 100644 --- a/src/js/state/stores/get-persistable.ts +++ b/src/js/state/stores/get-persistable.ts @@ -13,6 +13,7 @@ export const GLOBAL_PERSIST: StateKey[] = [ "queryVersions", "remoteQueries", "sessionQueries", + "poolSettings", ] export const WINDOW_PERSIST: StateKey[] = [ diff --git a/src/js/state/stores/root-reducer.ts b/src/js/state/stores/root-reducer.ts index e6e18cddc5..5985110035 100644 --- a/src/js/state/stores/root-reducer.ts +++ b/src/js/state/stores/root-reducer.ts @@ -19,6 +19,7 @@ import Loads from "../Loads" import QueryVersions from "../QueryVersions" import SessionQueries from "../SessionQueries" import SessionHistories from "../SessionHistories" +import PoolSettings from "../PoolSettings" const rootReducer = combineReducers({ appearance: Appearance.reducer, @@ -30,6 +31,7 @@ const rootReducer = combineReducers({ notice: Notice.reducer, tabs: Tabs.reducer, pools: Pools.reducer, + poolSettings: PoolSettings.reducer, loads: Loads.reducer, lakeStatuses: LakeStatuses.reducer, queries: Queries.reducer, diff --git a/src/js/state/types.ts b/src/js/state/types.ts index 2919931fe4..28aa7a1031 100644 --- a/src/js/state/types.ts +++ b/src/js/state/types.ts @@ -18,6 +18,7 @@ import {LakeStatusesState} from "./LakeStatuses/types" import {SessionQueriesState} from "./SessionQueries/types" import {QueryVersionsState} from "./QueryVersions/types" import {SessionHistoriesState} from "./SessionHistories/types" +import {PoolSettingsState} from "./PoolSettings/types" export type ThunkExtraArg = { api: ZuiApi @@ -39,6 +40,7 @@ export type State = { lakes: LakesState errors: ErrorsState pools: PoolsState + poolSettings: PoolSettingsState loads: LoadsState modal: ModalState notice: NoticeState diff --git a/src/panes/histogram-pane/chart.tsx b/src/panes/histogram-pane/chart.tsx new file mode 100644 index 0000000000..11f72ef016 --- /dev/null +++ b/src/panes/histogram-pane/chart.tsx @@ -0,0 +1,110 @@ +import {useDispatch} from "src/app/core/state" +import {D3StackedHistogram} from "./d3-stacked-histogram" +import {useTooltip} from "./use-tooltip" +import {formatData} from "./format-data" +import {DataProps} from "./use-data-props" +import * as d3 from "d3" +import {createPortal} from "react-dom" +import submitSearch from "src/app/query-home/flows/submit-search" +import Editor from "src/js/state/Editor" +import {Tooltip} from "./tooltip" +import styles from "./histogram-pane.module.css" +import {memo, useMemo} from "react" + +export const Chart = memo(function Chart( + props: { + width: number + height: number + } & DataProps +) { + const {width, height, range, data, interval, colorMap} = props + const dispatch = useDispatch() + const tooltip = useTooltip() + + const histogramProps = useMemo( + () => { + const {keys, map, widePoints} = formatData(data) + const maxY = d3.max(widePoints, (v) => v.sum) + const margin = {top: 32, bottom: 24, right: 18, left: 18} + const yScale = d3.scaleLinear().domain([0, maxY]) + const xScale = d3 + .scaleUtc() + .domain([interval(range[0]), interval.offset(interval(range[1]))]) + + const defaultColorScale = d3 + .scaleOrdinal() + .domain(keys) + .range(d3.schemeTableau10) + + const colorScale = (key: string) => { + const color = colorMap && colorMap[key] + return color || defaultColorScale(key) + } + + function onBrushMove(e: d3.D3BrushEvent) { + if (!e.selection) tooltip.show() + else tooltip.hide() + } + + function onBrushEnd([from, to]: [Date, Date]) { + tooltip.show() + const field = props.timeField + dispatch(Editor.setTimeRange({field, from, to})) + dispatch(submitSearch()) + } + + function onBrushPointerMove(e: PointerEvent) { + const [x] = d3.pointer(e) + const ts = interval.floor(xScale.invert(x)) + let data = map.get(ts.getTime()) ?? null + tooltip.setData(data) + tooltip.move(e) + } + + function onBrushPointerEnter() { + tooltip.show() + } + + function onBrushPointerLeave() { + tooltip.hide() + } + + return { + onBrushPointerMove, + onBrushPointerEnter, + onBrushPointerLeave, + onBrushEnd, + onBrushMove, + colorScale, + xScale, + yScale, + data: widePoints, + keys, + interval, + margin, + } + }, + // Only re-render the histogram when the data prop changes. + [props.data] + ) + + return ( + <> + + {createPortal( + , + document.getElementById("tooltip-root") + )} + + ) +}) diff --git a/src/panes/histogram-pane/d3-stacked-histogram.tsx b/src/panes/histogram-pane/d3-stacked-histogram.tsx new file mode 100644 index 0000000000..cdc9b1f317 --- /dev/null +++ b/src/panes/histogram-pane/d3-stacked-histogram.tsx @@ -0,0 +1,169 @@ +import {memo, useLayoutEffect, useRef} from "react" +import * as d3 from "d3" + +export const D3StackedHistogram = memo(function D3StackedHistogram(props: { + width: number + height: number + margin: {left: number; right: number; top: number; bottom: number} + xScale: d3.ScaleTime + yScale: d3.ScaleLinear + colorScale: (key: string) => string + data: any[] + keys: string[] + interval: d3.TimeInterval + className: string + onBrushPointerEnter?: (e: PointerEvent) => void + onBrushPointerMove?: (e: PointerEvent) => void + onBrushPointerLeave?: (e: PointerEvent) => void + onBrushEnd: (extent: [Date, Date]) => void + onBrushMove: (e: d3.D3BrushEvent) => void + "aria-label"?: string +}) { + // Dimensions + const {width, height, margin} = props + const innerWidth = width - margin.left - margin.right + const innerHeight = height - margin.top - margin.bottom + + // Scales + const {xScale, yScale, colorScale, interval} = props + xScale.range([0, innerWidth]) + yScale.range([innerHeight, 0]) + const barWidth = xScale(interval.offset(xScale.domain()[0])) + const data = d3.stack().keys(props.keys).order(d3.stackOrderAscending)( + props.data + ) + + // Create the static elements on mount + const ref = useRef(null) + useLayoutEffect(() => { + const el = ref.current + if (!el) return + const svg = d3.select(el) + svg.append("g").attr("class", "histogram") + svg.append("g").attr("class", "x-axis") + svg.append("g").attr("class", "y-axis") + svg.append("rect").attr("class", "hoverline") + svg.append("g").attr("class", "brush") + svg.append("text").attr("class", "x-label") + svg.append("text").attr("class", "y-label") + + return () => { + if (el) el.innerHTML = "" + } + }, []) + + // Render the chart when things are updated + useLayoutEffect(() => { + const svg = d3.select(ref.current) + /** + * Render the x axis + */ + svg + .select(".x-axis") + .attr("transform", `translate(${margin.left}, ${height - margin.bottom})`) + .call(d3.axisBottom(xScale).ticks(4)) + + /** + * Render the y axis + */ + const format = d3.format(".3~s") + const maxY = yScale.domain()[1] + svg + .select(".y-axis") + .attr("transform", `translate(${margin.left}, ${margin.top})`) + .call(d3.axisRight(yScale).tickValues([])) + + d3.select(".y-label") + .text(format(maxY)) + .attr("x", margin.left) + .attr("y", margin.top - 4) + .attr("font-size", 10) + + /** + * Render the bars + */ + svg + .select(".histogram") + .attr("transform", `translate(${margin.left}, ${margin.top})`) + .selectAll("g") + .data(data, (d: {key: string}) => d.key) + .join("g") + .style("stroke", (d) => d3.rgb(colorScale(d.key)).darker().toString()) + .style("fill", (d) => colorScale(d.key)) + .selectAll("rect") + .data((d) => d) + .join("rect") + .attr("x", (d) => xScale(d.data.time)) + .attr("width", barWidth) + .attr("y", (d) => yScale(d[1])) + .attr("height", (d) => yScale(d[0]) - yScale(d[1])) + + /** + * Render the brush layer + */ + const brush = d3 + .brushX() + .extent([ + [0, 0], + [innerWidth, innerHeight], + ]) + .on("end", (e: d3.D3BrushEvent) => { + if (e.selection) { + props.onBrushEnd([ + xScale.invert(e.selection[0] as number), + xScale.invert(e.selection[1] as number), + ]) + brush.move(svg.selectAll(".brush"), null) + } + }) + .on("brush", (e: d3.D3BrushEvent) => { + call(props.onBrushMove, e) + }) + + svg + .select(".brush") + .attr("transform", `translate(${margin.left}, ${margin.top})`) + .call(brush) + + /** + * Render the mouseover layer + */ + const line = svg.select(".hoverline") + svg + .select(".brush") + .on("pointerenter", (e: PointerEvent) => + call(props.onBrushPointerEnter, e) + ) + .on("pointermove", (e: PointerEvent) => { + call(props.onBrushPointerMove, e) + const [x] = d3.pointer(e) + line + .attr("x", margin.left + x) + .attr("y", margin.top) + .attr("width", 1) + .attr("height", innerHeight) + .attr("opacity", 1) + }) + .on("pointerleave", (e: PointerEvent) => { + call(props.onBrushPointerLeave, e) + line.attr("opacity", 0) + }) + }) + + return ( + + ) +}) + +function call any>( + fn: Fn, + ...args: Parameters +) { + if (fn) fn(...args) +} diff --git a/src/panes/histogram-pane/error.tsx b/src/panes/histogram-pane/error.tsx new file mode 100644 index 0000000000..67d66ff19f --- /dev/null +++ b/src/panes/histogram-pane/error.tsx @@ -0,0 +1,11 @@ +import styles from "./histogram-pane.module.css" + +export function Error(props: {message: string}) { + return ( +
+
+

{props.message}

+
+
+ ) +} diff --git a/src/panes/histogram-pane/format-data.ts b/src/panes/histogram-pane/format-data.ts new file mode 100644 index 0000000000..d462ed6023 --- /dev/null +++ b/src/panes/histogram-pane/format-data.ts @@ -0,0 +1,50 @@ +import {Point, WidePoint} from "./types" +import * as zed from "@brimdata/zed-js" +import * as d3 from "d3" + +export function formatData(points: zed.Value[]) { + const data = points.map(formatDatum) as Point[] + const keys = getKeys(data) + const map = groupByTimeAndWiden(data, keys) + const widePoints = Array.from(map.values()) + + return {keys, map, widePoints} +} + +function getKeys(data: Point[]) { + return Array.from(new Set(data.map((d) => d.group))).sort() +} + +function getDefaultWidePoint(keys: string[]) { + return keys.reduce((wide, key: string) => ({...wide, [key]: 0}), { + time: new Date(), + sum: 0, + } as WidePoint) +} + +function groupByTimeAndWiden(data: Point[], keys: string[]) { + const defaults = getDefaultWidePoint(keys) + return d3.rollup( + data, + (values) => values.reduce(widen, defaults), + (v) => (v.time instanceof Date ? v.time.getTime() : null) + ) +} + +function widen(wide: WidePoint, point: Point) { + const sum = wide.sum + point.count + const time = point.time + return {...wide, [point.group]: point.count, sum, time} as WidePoint +} + +export function formatDatum(point: zed.Record) { + return { + time: point.get("time").toJS(), + group: formatGroup(point.get("group")), + count: point.get("count").toJS(), + } +} + +export function formatGroup(value: zed.Value) { + return value.toString() +} diff --git a/src/panes/histogram-pane/get-interval.ts b/src/panes/histogram-pane/get-interval.ts new file mode 100644 index 0000000000..c4dc7b77db --- /dev/null +++ b/src/panes/histogram-pane/get-interval.ts @@ -0,0 +1,88 @@ +import moment from "moment" + +import * as d3 from "d3" + +export type Interval = { + number: number + unit: LongTimeUnit + fn: d3.TimeInterval +} + +export type LongTimeUnit = + | "millisecond" + | "second" + | "minute" + | "hour" + | "day" + | "month" + +export const timeUnits = { + millisecond: "ms", + second: "s", + minute: "m", + hour: "h", + day: "d", + week: "w", + year: "y", +} + +const ms = 1 +const sec = 1000 * ms +const min = 60 * sec +const hr = 60 * min +const day = 24 * hr + +export function getInterval([from, to]: [Date, Date]): Interval { + const duration = moment.duration(moment(to).diff(moment(from))) + + if (duration.asMinutes() <= 1) + return {number: 100, unit: "millisecond", fn: d3.utcMillisecond.every(100)} + + if (duration.asMinutes() <= 3) + return {number: 500, unit: "millisecond", fn: d3.utcMillisecond.every(500)} + + if (duration.asMinutes() <= 5) + return {number: 1, unit: "second", fn: d3.utcMillisecond.every(1 * sec)} + + if (duration.asMinutes() <= 10) + return {number: 10, unit: "second", fn: d3.utcMillisecond.every(10 * sec)} + + if (duration.asMinutes() <= 20) + return {number: 20, unit: "second", fn: d3.utcMillisecond.every(20 * sec)} + + if (duration.asMinutes() <= 30) + return {number: 30, unit: "second", fn: d3.utcMillisecond.every(30 * sec)} + + if (duration.asHours() <= 2) + return {number: 1, unit: "minute", fn: d3.utcMillisecond.every(1 * min)} + + if (duration.asHours() <= 4) + return {number: 5, unit: "minute", fn: d3.utcMillisecond.every(5 * min)} + + if (duration.asHours() <= 12) + return {number: 15, unit: "minute", fn: d3.utcMillisecond.every(15 * min)} + + if (duration.asDays() <= 1) + return {number: 30, unit: "minute", fn: d3.utcMillisecond.every(30 * min)} + + if (duration.asDays() <= 3) + return {number: 1, unit: "hour", fn: d3.utcMillisecond.every(1 * hr)} + + if (duration.asDays() <= 14) + return {number: 6, unit: "hour", fn: d3.utcMillisecond.every(6 * hr)} + + if (duration.asDays() <= 60) + return {number: 12, unit: "hour", fn: d3.utcMillisecond.every(12 * hr)} + + if (duration.asDays() <= 120) + return {number: 1, unit: "day", fn: d3.utcMillisecond.every(1 * day)} + + if (duration.asMonths() <= 12) + return {number: 7, unit: "day", fn: d3.utcMillisecond.every(7 * day)} + + return { + number: 30, + unit: "day", + fn: d3.utcMillisecond.every(30 * day), + } +} diff --git a/src/panes/histogram-pane/histogram-pane.module.css b/src/panes/histogram-pane/histogram-pane.module.css new file mode 100644 index 0000000000..d914154863 --- /dev/null +++ b/src/panes/histogram-pane/histogram-pane.module.css @@ -0,0 +1,78 @@ +.pane { + height: max(100px, 15vh); + width: 100%; + position: relative; + border-top: 1px solid var(--border-color); +} + + + +.dialog { + z-index: 99999; + position: fixed; + width: 180px; + border: none; + border-radius: 8px; + box-shadow: var(--shadow-elevation-medium); + background: white; +} + +.settingsForm { + display: flex; + flex-direction: column; + gap: 16px; +} + +.tooltip { + position: absolute; + top: 0; + left: 0; +} + +.colorKey { + display: inline-block; + padding: 1px 6px; + border-radius: 4px; + min-width: 60px; +} + +.errorContainer { + height: 100%; + width: 100%; + display: flex; + padding: 1rem; + align-items: center; + justify-content: center; +} + +.errorMessage { + text-align: center; + opacity: 0.5; +} + +.graphic { + user-select: none; + color: var(--foreground-color); +} + +.graphic :global(.domain), +.graphic line { + stroke: var(--border-color); +} + +div.toolbar { + position: absolute; + width: 100%; + background-color: transparent; + border-bottom: none; + height: auto; + margin-top: 2px; + gap: 4px; + justify-content: end; +} + +.title { + font-size: 0.7rem; + color: var(--foreground-color-light); + /* color: var(--border-color); */ +} diff --git a/src/panes/histogram-pane/histogram.tsx b/src/panes/histogram-pane/histogram.tsx new file mode 100644 index 0000000000..e440019bbf --- /dev/null +++ b/src/panes/histogram-pane/histogram.tsx @@ -0,0 +1,20 @@ +import {useDataTransition} from "src/util/hooks/use-data-transition" +import {Chart} from "./chart" +import {Error} from "./error" +import {useDataProps} from "./use-data-props" +import {validateDataProps} from "./validate-data" + +export function Histogram(props: {width: number; height: number}) { + const data = useDataProps() + const dataProps = useDataTransition( + data, + data.isFetching && data.data.length === 0 + ) + const error = validateDataProps(dataProps) + + if (error) { + return + } else { + return + } +} diff --git a/src/panes/histogram-pane/pane.tsx b/src/panes/histogram-pane/pane.tsx new file mode 100644 index 0000000000..6cfabddd92 --- /dev/null +++ b/src/panes/histogram-pane/pane.tsx @@ -0,0 +1,27 @@ +import {useSelector} from "react-redux" +import styles from "./histogram-pane.module.css" +import Layout from "src/js/state/Layout" +import {SettingsButton} from "./settings-button" +import {useParentSize} from "src/util/hooks/use-parent-size" +import {Histogram} from "./histogram" +import {Toolbar} from "src/components/toolbar" +import {Title} from "./title" + +export function HistogramPane() { + const {Parent, width, height} = useParentSize() + const show = useSelector(Layout.getShowHistogram) + + if (!show) return null + + return ( +
+ + + + + </Toolbar> + <Histogram width={width} height={height} /> + </Parent> + </div> + ) +} diff --git a/src/panes/histogram-pane/run-query.ts b/src/panes/histogram-pane/run-query.ts new file mode 100644 index 0000000000..7ef1736937 --- /dev/null +++ b/src/panes/histogram-pane/run-query.ts @@ -0,0 +1,101 @@ +import Current from "src/js/state/Current" +import PoolSettings from "src/js/state/PoolSettings" +import {QueryModel} from "src/js/models/query-model" +import {getInterval, timeUnits} from "./get-interval" +import Histogram from "src/js/state/Histogram" +import {QueryPin, TimeRangeQueryPin} from "src/js/state/Editor/types" +import Results from "src/js/state/Results" +import ZuiApi from "src/js/api/zui-api" +import {isAbortError} from "src/util/is-abort-error" + +export const HISTOGRAM_RESULTS = "histogram" + +export async function runHistogramQuery(api: ZuiApi) { + const id = HISTOGRAM_RESULTS + const tabId = api.current.tabId + const key = api.current.location.key + const version = api.select(Current.getVersion) + const poolId = api.select(Current.getPoolFromQuery)?.id + const baseQuery = QueryModel.versionToZed(version) + const {timeField, colorField} = api.select((s) => + PoolSettings.findWithDefaults(s, poolId) + ) + + function setup() { + api.dispatch(Results.init({id, tabId, key, query: ""})) + api.dispatch(Histogram.init()) + } + + function collect({rows}) { + api.dispatch(Results.setValues({id, tabId, values: rows})) + } + + function error(error: Error) { + if (isAbortError(error)) return success() + api.dispatch(Results.error({id, tabId, error: error.message})) + } + + function success() { + api.dispatch(Results.success({id, tabId})) + } + + function isRangePin(p: QueryPin) { + return p.type === "time-range" && !p.disabled && p.field === timeField + } + + function getPinRange() { + const rangePin = version.pins.find(isRangePin) as TimeRangeQueryPin + return rangePin + ? ([new Date(rangePin.from), new Date(rangePin.to)] as [Date, Date]) + : null + } + + async function getPoolRange() { + const query = `from ${poolId} | min(${timeField}), max(${timeField})` + const resp = await api.query(query, {id, tabId}) + const [{min, max}] = await resp.js() + if (!(min instanceof Date && max instanceof Date)) return null + return [min, max] as [Date, Date] + } + + async function getNullTimeCount() { + const query = `${baseQuery} | ${timeField} == null | count()` + const id = "null-time-count" + const resp = await api.query(query, {id, tabId}) + const [count] = await resp.js() + api.dispatch(Histogram.setNullXCount(count ?? 0)) + } + + async function getMissingTimeCount() { + const query = `${baseQuery} | !has(${timeField}) | count()` + const id = "missing-time-count" + const resp = await api.query(query, {id, tabId}) + const [count] = await resp.js() + api.dispatch(Histogram.setMissingXCount(count ?? 0)) + } + + async function run() { + const range = getPinRange() || (await getPoolRange()) + if (!range) + throw new Error(`Unable to determine date range using '${timeField}'.`) + + const {unit, number, fn} = getInterval(range) + const interval = `${number}${timeUnits[unit]}` + const query = `${baseQuery} | ${timeField} != null | count() by time := bucket(${timeField}, ${interval}), group := ${colorField} | sort time` + const resp = await api.query(query, {id, tabId}) + api.dispatch(Histogram.setInterval({unit, number, fn})) + api.dispatch(Histogram.setRange(range)) + resp.collect(collect, {}) + getNullTimeCount() + getMissingTimeCount() + await resp.promise + } + + try { + setup() + await run() + success() + } catch (e) { + error(e) + } +} diff --git a/src/panes/histogram-pane/settings-button.tsx b/src/panes/histogram-pane/settings-button.tsx new file mode 100644 index 0000000000..cc99bc1261 --- /dev/null +++ b/src/panes/histogram-pane/settings-button.tsx @@ -0,0 +1,38 @@ +import {useRef, useState} from "react" +import styles from "./histogram-pane.module.css" +import {Dialog} from "src/components/dialog/dialog" +import {SettingsForm} from "./settings-form" +import {useSelector} from "react-redux" +import Current from "src/js/state/Current" +import {IconButton} from "src/components/icon-button" + +export function SettingsButton() { + const [isOpen, setIsOpen] = useState(false) + const button = useRef() + const close = () => setIsOpen(false) + const poolId = useSelector(Current.getPoolFromQuery)?.id // might be null + + return ( + <> + <IconButton + iconName="three-dots-stacked" + onClick={() => setIsOpen(true)} + label="Histogram Settings" + ref={button} + /> + <Dialog + onOutsideClick={close} + onClose={close} + className={styles.dialog} + isOpen={isOpen} + anchor={button.current} + anchorPoint="center left" + dialogPoint="center right" + dialogMargin="0 10px" + keepOnScreen={false} + > + <SettingsForm close={close} poolId={poolId} key={poolId} /> + </Dialog> + </> + ) +} diff --git a/src/panes/histogram-pane/settings-form.tsx b/src/panes/histogram-pane/settings-form.tsx new file mode 100644 index 0000000000..50955bec08 --- /dev/null +++ b/src/panes/histogram-pane/settings-form.tsx @@ -0,0 +1,67 @@ +import {useSelector} from "react-redux" +import {Field} from "src/components/field" +import InputLabel from "src/js/components/common/forms/InputLabel" +import TextInput from "src/js/components/common/forms/TextInput" +import PoolSettings from "src/js/state/PoolSettings" +import {useForm} from "react-hook-form" +import {useDispatch} from "src/app/core/state" +import {State} from "src/js/state/types" +import styles from "./histogram-pane.module.css" +import {runHistogramQuery} from "./run-query" +import {getDefaults} from "src/js/state/PoolSettings/selectors" +import {InputButton} from "src/components/input-button" +import {useZuiApi} from "src/app/core/context" + +type Inputs = { + timeField: string + colorField: string +} + +type Props = { + close: () => void + poolId: string +} + +const defaults = getDefaults() + +export function SettingsForm(props: Props) { + const settings = useSelector((s: State) => PoolSettings.find(s, props.poolId)) + const dispatch = useDispatch() + const api = useZuiApi() + const form = useForm<Inputs>({defaultValues: settings}) + + function onSubmit(data: Inputs) { + const timeField = data.timeField.trim() || defaults.timeField + const colorField = data.colorField.trim() || defaults.colorField + const id = props.poolId + dispatch(PoolSettings.upsert({id, timeField, colorField})) + props.close() + runHistogramQuery(api) + } + + return ( + <form + method="dialog" + onSubmit={form.handleSubmit(onSubmit)} + className={styles.settingsForm} + > + <Field> + <InputLabel>Time Field</InputLabel> + <TextInput + {...form.register("timeField")} + placeholder={defaults.timeField} + /> + </Field> + <Field> + <InputLabel>Color Field</InputLabel> + <TextInput + {...form.register("colorField")} + placeholder={defaults.colorField} + /> + </Field> + <Field> + <InputButton type="submit">Save</InputButton> + </Field> + </form> + ) +} diff --git a/src/panes/histogram-pane/title.tsx b/src/panes/histogram-pane/title.tsx new file mode 100644 index 0000000000..377c10510a --- /dev/null +++ b/src/panes/histogram-pane/title.tsx @@ -0,0 +1,45 @@ +import PoolSettings from "src/js/state/PoolSettings" +import styles from "./histogram-pane.module.css" +import {useSelector} from "react-redux" +import {State} from "src/js/state/types" +import Current from "src/js/state/Current" +import Histogram from "src/js/state/Histogram" +import * as d3 from "d3" + +// Make all this data change together +// The null count should not appear if there is no data + +export function Title() { + const poolId = useSelector(Current.getPoolFromQuery)?.id + const {timeField, colorField} = useSelector((s: State) => + PoolSettings.findWithDefaults(s, poolId) + ) + const nullCount = useSelector(Histogram.getNullXCount) + const missingCount = useSelector(Histogram.getMissingXCount) + const format = d3.format(",") + + const content = [] + + if (nullCount) { + content.push( + <> + {format(nullCount)} null {timeField} values •{" "} + </> + ) + } + if (missingCount) { + content.push( + <> + {format(missingCount)} missing {timeField} values •{" "} + </> + ) + } + + content.push( + <> + counts by <i>{timeField}</i> and <i>{colorField}</i> + </> + ) + + return <p className={styles.title}>{content}</p> +} diff --git a/src/panes/histogram-pane/tooltip.tsx b/src/panes/histogram-pane/tooltip.tsx new file mode 100644 index 0000000000..4a8a154d91 --- /dev/null +++ b/src/panes/histogram-pane/tooltip.tsx @@ -0,0 +1,53 @@ +import {CSSProperties} from "react" +import styles from "./histogram-pane.module.css" +import time from "src/js/models/time" +import {withCommas} from "src/js/lib/fmt" +import {WidePoint} from "./types" + +export const Tooltip = (props: { + style: CSSProperties + data: WidePoint + colorScale: (key: string) => string +}) => { + if (!props.data) return null + const segments = Object.entries(props.data) + .filter( + ([name, count]) => + name !== "time" && name !== "sum" && (count as number) > 0 + ) + .map(([name, count]) => ({name, count} as {name: string; count: number})) + .sort((a, b) => b.count - a.count) + + const timeLabel = props.data.time + ? time(props.data.time).format("MMM D, YYYY • HH:mm") + : "null" + return ( + <div style={props.style} className={styles.tooltip + " histogram-tooltip"}> + <p className="ts">{timeLabel}</p> + <table> + <tbody> + {segments.map(({name, count}) => { + return ( + <tr key={name}> + <td> + <span + className={styles.colorKey} + style={{backgroundColor: props.colorScale(name)}} + > + {name.substring(0, 20)} + </span> + </td> + <td className="count">{withCommas(count)}</td> + </tr> + ) + })} + <tr> + <td colSpan={2} className="total-row"> + {withCommas(props.data.sum)} + </td> + </tr> + </tbody> + </table> + </div> + ) +} diff --git a/src/panes/histogram-pane/types.ts b/src/panes/histogram-pane/types.ts new file mode 100644 index 0000000000..3d5ddee5a2 --- /dev/null +++ b/src/panes/histogram-pane/types.ts @@ -0,0 +1,2 @@ +export type Point = {time: Date; count: number; group: string} +export type WidePoint = {time: Date; sum: number} & {[group: string]: number} diff --git a/src/panes/histogram-pane/use-data-props.ts b/src/panes/histogram-pane/use-data-props.ts new file mode 100644 index 0000000000..7b87c939e8 --- /dev/null +++ b/src/panes/histogram-pane/use-data-props.ts @@ -0,0 +1,31 @@ +import {useSelector} from "react-redux" +import Current from "src/js/state/Current" +import PoolSettings from "src/js/state/PoolSettings" +import Histogram from "src/js/state/Histogram" +import * as zed from "@brimdata/zed-js" +import {State} from "src/js/state/types" +import {HISTOGRAM_RESULTS} from "./run-query" +import Results from "src/js/state/Results" + +export type DataProps = ReturnType<typeof useDataProps> + +export function useDataProps() { + const poolId = useSelector(Current.getPoolFromQuery)?.id + const error = useSelector(Histogram.getError) + const settings = useSelector((s: State) => + PoolSettings.findWithDefaults(s, poolId) + ) + const isFetching = useSelector(Results.isFetching(HISTOGRAM_RESULTS)) + + return { + range: useSelector(Histogram.getRange), + interval: useSelector(Histogram.getInterval)?.fn, + data: useSelector(Histogram.getData) as zed.Record[], + timeField: settings.timeField, + colorField: settings.colorField, + colorMap: settings.colorMap, + isFetching, + poolId, + error, + } +} diff --git a/src/panes/histogram-pane/use-tooltip.ts b/src/panes/histogram-pane/use-tooltip.ts new file mode 100644 index 0000000000..4bc179c516 --- /dev/null +++ b/src/panes/histogram-pane/use-tooltip.ts @@ -0,0 +1,31 @@ +import {useState} from "react" + +export function useTooltip() { + const tooltipWidth = 200 + const yPad = -60 + const xPad = 20 + const [style, setStyle] = useState({width: tooltipWidth}) + const [data, setData] = useState(null) + const updateStyle = (css) => setStyle((prev) => ({...prev, ...css})) + const translate = (x: number, y: number) => + updateStyle({transform: `translate(${x}px, ${y}px)`}) + + return { + data, + setData, + style, + hide: () => updateStyle({opacity: 0}), + show: () => updateStyle({opacity: 1}), + move: (e: PointerEvent) => { + const brush = e.currentTarget as SVGGElement + const {y} = brush.getBoundingClientRect() + const x = e.pageX + const docWidth = document.body.clientWidth + if (x + xPad + tooltipWidth < docWidth) { + translate(x + xPad, y + yPad) + } else { + translate(x - tooltipWidth - xPad, y + yPad) + } + }, + } +} diff --git a/src/panes/histogram-pane/validate-data.ts b/src/panes/histogram-pane/validate-data.ts new file mode 100644 index 0000000000..9f4ea66cf7 --- /dev/null +++ b/src/panes/histogram-pane/validate-data.ts @@ -0,0 +1,48 @@ +import * as zed from "@brimdata/zed-js" +import {DataProps} from "./use-data-props" + +const MAX_CARDINALITY = 40 + +export function validateDataProps(props: DataProps) { + const {error, range, interval, timeField, colorField, data} = props + if (props.isFetching) { + return "Loading..." + } + if (error) { + return error + } + if (!range || !interval) { + return `No date range found with '${timeField}'.` + } + if (data.length === 0) { + return "No data." + } + if (!hasTimeField(data)) { + return `Field '${timeField}' did not return time values.` + } + if (!hasGroupField(data)) { + return `Field '${colorField}' did not return any groups.` + } + if (hasHighCardinality(data, MAX_CARDINALITY)) { + return `Field '${colorField}' returned too many unique values (>${MAX_CARDINALITY}).` + } + return null +} + +function hasTimeField(data: zed.Record[]) { + return data.some((r) => r.has("time", zed.TypeTime)) +} + +function hasGroupField(data: zed.Record[]) { + return data.some((r) => r.has("group")) +} + +function hasHighCardinality(data: zed.Record[], max: number) { + const set = new Set() + for (const record of data) { + const key = record.get("group").toString() + set.add(key) + if (set.size > max) return true + } + return false +} diff --git a/src/panes/results-pane/context.tsx b/src/panes/results-pane/context.tsx index ec4fad9eda..80e56c0f65 100644 --- a/src/panes/results-pane/context.tsx +++ b/src/panes/results-pane/context.tsx @@ -1,34 +1,31 @@ import React, {ReactNode, useContext, useMemo} from "react" import {useSelector} from "react-redux" -import useSelect from "src/app/core/hooks/use-select" -import {useDispatch} from "src/app/core/state" +import {useNextPage} from "src/core/query/use-query" +import {useResults} from "src/core/query/use-results" import Layout from "src/js/state/Layout" import Results from "src/js/state/Results" -import {MAIN_RESULTS} from "src/js/state/Results/types" +import {RESULTS_QUERY} from "src/panes/results-pane/run-results-query" +import {useDataTransition} from "src/util/hooks/use-data-transition" import useResizeObserver from "use-resize-observer" function useContextValue(parentRef: React.RefObject<HTMLDivElement>) { const rect = useResizeObserver({ref: parentRef}) - const shapesObj = useSelector(Results.getShapes(MAIN_RESULTS)) - const shapes = useMemo(() => Object.values(shapesObj), [shapesObj]) - const select = useSelect() - const dispatch = useDispatch() + const nextPage = useNextPage(RESULTS_QUERY) + const fetching = useSelector(Results.isFetching(RESULTS_QUERY)) + const r = useResults(RESULTS_QUERY) + const results = useDataTransition(r, r.data.length === 0 && fetching) + const shapes = useMemo(() => Object.values(results.shapes), [results.shapes]) return { width: rect.width ?? 1000, height: rect.height ?? 1000, view: useSelector(Layout.getResultsView), - error: useSelector(Results.getError(MAIN_RESULTS)), - values: useSelector(Results.getValues(MAIN_RESULTS)), + error: results.error, + values: results.data, shapes, isSingleShape: shapes.length === 1, firstShape: shapes[0], - loadMore: () => { - if (select(Results.isFetching(MAIN_RESULTS))) return - if (select(Results.isComplete(MAIN_RESULTS))) return - if (select(Results.isLimited(MAIN_RESULTS))) return - dispatch(Results.fetchNextPage()) - }, + loadMore: nextPage, } } diff --git a/src/panes/results-pane/results-pane.module.css b/src/panes/results-pane/results-pane.module.css new file mode 100644 index 0000000000..b96aa13fcb --- /dev/null +++ b/src/panes/results-pane/results-pane.module.css @@ -0,0 +1,3 @@ +.container { + height: 100%; +} diff --git a/src/panes/results-pane/results-pane.tsx b/src/panes/results-pane/results-pane.tsx index a6f349f63f..73f3b95e62 100644 --- a/src/panes/results-pane/results-pane.tsx +++ b/src/panes/results-pane/results-pane.tsx @@ -5,11 +5,16 @@ import {Error} from "./error" import {Inspector} from "./inspector" import {Table} from "./table" import {TableInspector} from "./table-inspector" +import styles from "./results-pane.module.css" export function ResultsPane() { const ref = useRef() return ( - <div ref={ref} className="results-pane" data-testid="results-pane"> + <div + ref={ref} + className={"results-pane " + styles.container} + data-testid="results-pane" + > <AppErrorBoundary> <ResultsPaneProvider parentRef={ref}> <ResultsView /> diff --git a/src/panes/results-pane/run-results-query.tsx b/src/panes/results-pane/run-results-query.tsx new file mode 100644 index 0000000000..92b564c112 --- /dev/null +++ b/src/panes/results-pane/run-results-query.tsx @@ -0,0 +1,14 @@ +import {firstPage} from "src/core/query/run" +import {QueryModel} from "src/js/models/query-model" +import Current from "src/js/state/Current" +import {Thunk} from "src/js/state/types" + +export const RESULTS_QUERY = "zui-results/main" + +export function runResultsQuery(): Thunk { + return (dispatch, getState) => { + const version = Current.getVersion(getState()) + const query = QueryModel.versionToZed(version) + dispatch(firstPage({id: RESULTS_QUERY, query})) + } +} diff --git a/src/panes/results-pane/table-controller.tsx b/src/panes/results-pane/table-controller.tsx index 5897015c5b..e0475c14aa 100644 --- a/src/panes/results-pane/table-controller.tsx +++ b/src/panes/results-pane/table-controller.tsx @@ -1,10 +1,8 @@ import {useMemo} from "react" import * as zed from "@brimdata/zed-js" import {ZedTableHandlers, ZedTableState} from "src/components/zed-table/types" -import useSelect from "src/app/core/hooks/use-select" -import Results from "src/js/state/Results" import {useDispatch} from "src/app/core/state" -import {MAIN_RESULTS} from "src/js/state/Results/types" +import {RESULTS_QUERY} from "src/panes/results-pane/run-results-query" import {useResultsContext} from "src/app/query-home" import {headerContextMenu} from "src/app/menus/header-context-menu" import {useSelector} from "react-redux" @@ -12,6 +10,7 @@ import TableState from "src/js/state/Table" import {State} from "src/js/state/types" import {valueContextMenu} from "src/app/menus/value-context-menu" import {useResultsPaneContext} from "./context" +import {useNextPage} from "src/core/query/use-query" export function useTableState() { const {firstShape} = useResultsPaneContext() @@ -27,20 +26,15 @@ export function useTableState() { export function useTableHandlers() { const ctx = useResultsPaneContext() - const select = useSelect() const dispatch = useDispatch() const shape = ctx.firstShape + const nextPage = useNextPage(RESULTS_QUERY) return useMemo<ZedTableHandlers>( () => ({ onStateChange: (state) => { dispatch(TableState.setStateForShape({shape, state})) }, - onScrollNearBottom: () => { - if (select(Results.isFetching(MAIN_RESULTS))) return - if (select(Results.isComplete(MAIN_RESULTS))) return - if (select(Results.isLimited(MAIN_RESULTS))) return - dispatch(Results.fetchNextPage()) - }, + onScrollNearBottom: nextPage, onHeaderContextMenu(e, column) { headerContextMenu .build(this, column) diff --git a/src/plugins/brimcap/loader.ts b/src/plugins/brimcap/loader.ts index 4a8870ce72..a9e88cf0f6 100644 --- a/src/plugins/brimcap/loader.ts +++ b/src/plugins/brimcap/loader.ts @@ -7,7 +7,8 @@ import {ChildProcess} from "child_process" import {Loader} from "src/core/loader/types" import {LoadContext} from "src/core/loader/load-context" import {isPcap} from "./packets/is-pcap" -import {configurations, loaders} from "src/zui" +import {configurations, loaders, pools} from "src/zui" +import {zeekColorMap} from "./zeek/colors" function createLoader(root: string): Loader { const processes: Record<number, ChildProcess> = {} @@ -108,6 +109,13 @@ function createLoader(root: string): Loader { await ctx.onPoolChanged() ctx.onProgress(1) + + // update this pool's settings with zeek specific properties + pools + .configure(ctx.poolId) + .set("timeField", "ts") + .set("colorField", "_path") + .set("colorMap", zeekColorMap) } function rollback() {} diff --git a/src/plugins/brimcap/zeek/colors.ts b/src/plugins/brimcap/zeek/colors.ts new file mode 100644 index 0000000000..88355fa544 --- /dev/null +++ b/src/plugins/brimcap/zeek/colors.ts @@ -0,0 +1,33 @@ +export const zeekColorMap = { + conn: "#86c8b7", + dhcp: "#00578a", + dns: "#1ca0f2", + ftp: "#392277", + http: "#f8d330", + files: "#ad3f95", + mysql: "#d28204", + irc: "#00d1a6", + radius: "#ffd901", + kerberos: "#fbf758", + sip: "#006c7b", + smtp: "#e2e317", + ssl: "#080808", + ssh: "#535765", + syslog: "#ddb81d", + tunnel: "#007249", + dce_rpc: "#929292", + ntlm: "#6284a4", + rdp: "#081d5b", + smb_files: "#27eeff", + smb_mapping: "#0511d4", + weird: "#5e6373", + x509: "#eeb457", + pe: "#e65835", + dpd: "#256453", + notice: "red", + capture_loss: "purple", + software: "#65bef6", + stats: "#5ec4a8", + known_hosts: "#eeb457", + known_services: "#cc943a", +} diff --git a/src/plugins/core-pool/index.ts b/src/plugins/core-pool/index.ts new file mode 100644 index 0000000000..4b61a54256 --- /dev/null +++ b/src/plugins/core-pool/index.ts @@ -0,0 +1,10 @@ +import {FieldPath} from "src/core/field-path" +import {pools, PluginContext} from "src/zui" + +export function activate(_ctx: PluginContext) { + pools.on("create", ({pool}) => { + const poolKeyAccessor = new FieldPath(pool.layout.keys[0]).toString() + + pools.configure(pool.id).set("timeField", poolKeyAccessor) + }) +} diff --git a/src/test/system/system-test-class.tsx b/src/test/system/system-test-class.tsx index 9f06d4b97b..3aa9a44a6a 100644 --- a/src/test/system/system-test-class.tsx +++ b/src/test/system/system-test-class.tsx @@ -16,6 +16,7 @@ import {BootArgs, boot} from "./boot" import Tabs from "src/js/state/Tabs" import {createAndLoadFiles} from "src/app/commands/pools" import {ZuiMain} from "src/electron/zui-main" +import {teardown} from "./teardown" jest.setTimeout(20_000) @@ -60,6 +61,7 @@ export class SystemTest { afterEach(() => this.network.resetHandlers()) afterAll(async () => { + teardown() if (this.initialized) { await this.main.stop() tl.cleanup() diff --git a/src/test/system/teardown.ts b/src/test/system/teardown.ts new file mode 100644 index 0000000000..155a442662 --- /dev/null +++ b/src/test/system/teardown.ts @@ -0,0 +1,32 @@ +import {app, ipcMain, ipcRenderer} from "electron" +import {pools, session} from "src/zui" + +export function teardown() { + app.removeAllListeners() + teardownMockIpc(ipcRenderer) + teardownMockIpc(ipcMain) + teardownPluginApi() +} + +function teardownMockIpc(ipc: typeof ipcRenderer | typeof ipcMain) { + ipc + .eventNames() + .filter( + (name: string) => + !name.startsWith("__electron_mock_ipc__") && + ![ + "receive-from-main", + "error-from-main", + "send-to-main", + "receive-from-renderer", + "error-from-renderer", + "send-to-renderer", + ].includes(name) + ) + .forEach((name: string) => ipc.removeAllListeners(name)) +} + +function teardownPluginApi() { + session._teardown() + pools._teardown() +} diff --git a/src/test/unit/helpers/initTestStore.ts b/src/test/unit/helpers/initTestStore.ts index c5e38986ce..b265d7efb1 100644 --- a/src/test/unit/helpers/initTestStore.ts +++ b/src/test/unit/helpers/initTestStore.ts @@ -2,8 +2,10 @@ import {main as runMain} from "src/electron/run-main/run-main" import {ZuiMain} from "src/electron/zui-main" import initialize from "src/js/initializers/initialize" import {Store} from "src/js/state/types" +import {teardown} from "src/test/system/teardown" export default async (): Promise<Store> => { + teardown() const main = (await runMain({ lake: false, devtools: false, diff --git a/src/util/array-wrap.ts b/src/util/array-wrap.ts new file mode 100644 index 0000000000..fcbb5a3ad5 --- /dev/null +++ b/src/util/array-wrap.ts @@ -0,0 +1,4 @@ +export function arrayWrap<T>(value: T | T[]): T[] { + if (Array.isArray(value)) return value + else return [value] +} diff --git a/src/util/call.ts b/src/util/call.ts new file mode 100644 index 0000000000..db34e604cd --- /dev/null +++ b/src/util/call.ts @@ -0,0 +1,10 @@ +/** + * A safe way to call a function that does nothing if + * the function is undefined. + */ +export function call<Fn extends (...a: any[]) => any>( + fn: Fn, + ...args: Parameters<Fn> +) { + if (fn) fn(...args) +} diff --git a/src/util/hooks/use-data-transition.ts b/src/util/hooks/use-data-transition.ts new file mode 100644 index 0000000000..a2fff50ebc --- /dev/null +++ b/src/util/hooks/use-data-transition.ts @@ -0,0 +1,30 @@ +import {useEffect, useRef, useState} from "react" + +export function useDataTransition<T>( + real: T, + inTransition: boolean, + timeout = 150 +) { + const [timeExpired, setTimeExpired] = useState(false) + const prev = useRef(real) + + useEffect(() => { + if (!inTransition) prev.current = real + }, [inTransition, real]) + + useEffect(() => { + let id: any + if (inTransition) { + id = setTimeout(() => setTimeExpired(true), timeout) + } else { + setTimeExpired(false) + } + return () => clearTimeout(id) + }, [inTransition]) + + if (inTransition && !timeExpired) { + return prev.current + } else { + return real + } +} diff --git a/src/util/hooks/use-parent-size.tsx b/src/util/hooks/use-parent-size.tsx new file mode 100644 index 0000000000..0203ae9536 --- /dev/null +++ b/src/util/hooks/use-parent-size.tsx @@ -0,0 +1,29 @@ +import {useMemo} from "react" +import useResizeObserver from "use-resize-observer" + +export function useParentSize() { + const {ref, width, height} = useResizeObserver() + + const Parent = useMemo(() => { + return function Parent({children}) { + return ( + <div style={{position: "relative", height: "100%", width: "100%"}}> + <div + style={{ + position: "absolute", + left: "0", + right: "0", + bottom: "0", + top: "0", + pointerEvents: "none", + }} + ref={ref} + /> + {children} + </div> + ) + } + }, [ref]) + + return {Parent, width, height} +} diff --git a/src/util/is-abort-error.ts b/src/util/is-abort-error.ts new file mode 100644 index 0000000000..f55f2953c3 --- /dev/null +++ b/src/util/is-abort-error.ts @@ -0,0 +1,3 @@ +export function isAbortError(e: unknown) { + return e instanceof DOMException && e.message.match(/user aborted/) +} diff --git a/yarn.lock b/yarn.lock index 5971293ffd..d759704a83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -424,28 +424,28 @@ __metadata: languageName: node linkType: hard -"@brimdata/zed-js@npm:0.0.16": - version: 0.0.16 - resolution: "@brimdata/zed-js@npm:0.0.16" +"@brimdata/zed-js@npm:0.0.17": + version: 0.0.17 + resolution: "@brimdata/zed-js@npm:0.0.17" dependencies: "@types/event-source-polyfill": 1.0.1 event-source-polyfill: 1.0.31 events: 3.3.0 tslib: 2.5.0 - checksum: e7e44d0f819eefa1ebd21704039644482cc7fb809453d089d790054d42af61fccbaa7f8df1ff9a320bdb77c38da76a9f2a073a89663f2fec073ad284e661b87c + checksum: a75f57ebc5a1eca4abd4286d536a5b4ffba61478931264a1b0160fc16cb89656d07c2678a73719cd1374fade26bd2ad0232cf6b9751230a4cfbb9102e4816bfd languageName: node linkType: hard -"@brimdata/zed-node@npm:0.0.16": - version: 0.0.16 - resolution: "@brimdata/zed-node@npm:0.0.16" +"@brimdata/zed-node@npm:0.0.17": + version: 0.0.17 + resolution: "@brimdata/zed-node@npm:0.0.17" dependencies: - "@brimdata/zed-js": 0.0.16 + "@brimdata/zed-js": 0.0.17 fs-extra: ^11.1.1 node-fetch: ^2.6.2 peerDependencies: zed: "*" - checksum: 4339ebba2d9f404fb713b34b4c6e1dd7a38dc0d316103b092c3b9dc450a7c578497dd14a63ca7ec08b0c2235232c2b6e690b1203888b94effa935edd37b04a8e + checksum: b22da2060790a6ab1e0fc103ab3a9236e5eb7f4a005bf579bbe973ab90df953048d53224452bbcc6bcb7b16cd3c977115ed41b758d1089e76af4f7ca9364de7e languageName: node linkType: hard @@ -920,6 +920,24 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.2.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: ^3.3.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.4.0": + version: 4.5.1 + resolution: "@eslint-community/regexpp@npm:4.5.1" + checksum: 6d901166d64998d591fab4db1c2f872981ccd5f6fe066a1ad0a93d4e11855ecae6bfb76660869a469563e8882d4307228cebd41142adb409d182f2966771e57e + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^1.2.1": version: 1.2.1 resolution: "@eslint/eslintrc@npm:1.2.1" @@ -1940,283 +1958,275 @@ __metadata: languageName: node linkType: hard -"@types/d3-array@npm:*, @types/d3-array@npm:^1": - version: 1.2.7 - resolution: "@types/d3-array@npm:1.2.7" - checksum: c2f6ce06d9b096e75c18a4810a2e60d2dcbea524a8a139c34f161559f4fa7b10f105c6e3a4d630f8cfe819f887d1599c00ff4013142653fbd68ed35879051cd4 +"@types/d3-array@npm:^2": + version: 2.12.4 + resolution: "@types/d3-array@npm:2.12.4" + checksum: 3d8cf6f376785c83bacb7e0a51e4ac236f9f198f2f409463b27e4e1a23f49c40324afb320219b221d89334a988b6b874ed8fd16363e49861155f6dddd9125929 languageName: node linkType: hard -"@types/d3-axis@npm:*": - version: 1.0.12 - resolution: "@types/d3-axis@npm:1.0.12" +"@types/d3-axis@npm:^2": + version: 2.1.3 + resolution: "@types/d3-axis@npm:2.1.3" dependencies: - "@types/d3-selection": "*" - checksum: 63fb27d0c8a552c0745ff5d926b45c94e426fea3ef356de35f7ad7791f412c208bfa86002a177c0cb2d931c4a520bc26d63a903cb718ed4d906ee21a1b9a6411 + "@types/d3-selection": ^2 + checksum: 43627216884c4fc68af796d73686d6f1d8eec8e27f5a97d9c90477d9790d98dfe9276763c834609659e64010fc435eaec9e6d967fc92fd7375cc4c82e818e663 languageName: node linkType: hard -"@types/d3-brush@npm:*": - version: 1.1.1 - resolution: "@types/d3-brush@npm:1.1.1" +"@types/d3-brush@npm:^2": + version: 2.1.2 + resolution: "@types/d3-brush@npm:2.1.2" dependencies: - "@types/d3-selection": "*" - checksum: 498b46800965771929fe24ca1eca32d2889c0d33bdce12ec1c71ab0cf89c1073c4dd845736688019a2675d0c752c1adec0e7afe80d26b418377b046e2ee8dd5f + "@types/d3-selection": ^2 + checksum: 44bb9f845b03380f626ae422d2b2f7f2710b4a28fd8c4539e82567502a14bfab2150cc30726fa03c802ac063ad36f46407eb64596d09e38f19b8546fee942a37 languageName: node linkType: hard -"@types/d3-chord@npm:*": - version: 1.0.9 - resolution: "@types/d3-chord@npm:1.0.9" - checksum: 1e566792bb02f947adac07fe8401c39575a82cf6a506eb99a695b92d482cf2fdfdf13e5db23f827c8d22bf6bf521cac8d20d46fb8dfff9a41b4f47e58d0b4982 +"@types/d3-chord@npm:^2": + version: 2.0.3 + resolution: "@types/d3-chord@npm:2.0.3" + checksum: 65fa5ec389a4c86b9b2dc576fca9a9b5f0c50b2457ca48bce71b1501e08ef1a3448a5405ea2894d45838d38641923625b6282758a4fc5968553dd374bf629876 languageName: node linkType: hard -"@types/d3-collection@npm:*": - version: 1.0.8 - resolution: "@types/d3-collection@npm:1.0.8" - checksum: 403322b4cbe7ef875166abd21e5405d7a93d79a4d9b16244879b7c5c1e58a320e198017c946bc07314dc7efa373d87638ed02d0b6d00c7bac6975d0c64eb4f59 +"@types/d3-color@npm:^2": + version: 2.0.3 + resolution: "@types/d3-color@npm:2.0.3" + checksum: b4a963b15f4fe0e7e49b0898df3e51b46392d91c21038b7ec61aef0f13e04bd7bcfebf06c9fad9ee92317c9682a105e18942c9295a7e2715855622d4d6fc415a languageName: node linkType: hard -"@types/d3-color@npm:*": - version: 1.2.2 - resolution: "@types/d3-color@npm:1.2.2" - checksum: 41b29a5dec9aa8e5e57de75c3907a7d31abe2b765a4357d9dde9c03d4d6b5cd21d12db6ee1f2acf0f7517e9423008756ed2f2193c323501c2e4b3efa61e2e730 +"@types/d3-contour@npm:^2": + version: 2.0.4 + resolution: "@types/d3-contour@npm:2.0.4" + dependencies: + "@types/d3-array": ^2 + "@types/geojson": "*" + checksum: 73038fa2620cc1e7ec3b8fcbd837755e42a6c2a892c2cfdaf6c05ea6fd2c02fb1e7bd787687be6a83d47cfb929513a63dca984d3463516170b73dd36e7d10286 languageName: node linkType: hard -"@types/d3-contour@npm:*": - version: 1.3.0 - resolution: "@types/d3-contour@npm:1.3.0" - dependencies: - "@types/d3-array": "*" - "@types/geojson": "*" - checksum: 32c78962a69f7b343bf98809ecc06ac181878398c5fc935d6ee220d751df51a88846b5202ca529156c590e2750b0d2c40b8183ca784ae2dde93990e806c26c46 +"@types/d3-delaunay@npm:^5": + version: 5.3.1 + resolution: "@types/d3-delaunay@npm:5.3.1" + checksum: bf0f15b7e2b305974fe4a62315b95339eee9ebc46cbdaf1c439927aab0ece8e8664e875fe4a84607e195ae8ddf35c747d54c8bef07d19f925b7172528032f215 languageName: node linkType: hard -"@types/d3-dispatch@npm:*": - version: 1.0.8 - resolution: "@types/d3-dispatch@npm:1.0.8" - checksum: 279a26077d18a6b365cc06668f1b151202b0e25aa182172df689be2ed388eb005ede3ea3e6c7d0532c0f31a88f0e95b1096d0232ce254c52bb8be07a7dc5ad3e +"@types/d3-dispatch@npm:^2": + version: 2.0.1 + resolution: "@types/d3-dispatch@npm:2.0.1" + checksum: 4c6d1389e74a764a870203f655d4b4e12641a644ba355d384b1d2f572536e8549b9765263d96455627e2bd62a0e8cb98b93f6dea7c78992399494f6bf3a58169 languageName: node linkType: hard -"@types/d3-drag@npm:*": - version: 1.2.3 - resolution: "@types/d3-drag@npm:1.2.3" +"@types/d3-drag@npm:^2": + version: 2.0.2 + resolution: "@types/d3-drag@npm:2.0.2" dependencies: - "@types/d3-selection": "*" - checksum: b2ebe9acc28d73d43f152b25871e5882fb12beb6cf93d11bab0bd5a78a581b8e8d61b9ad48189ca4de26a722d49ab2c6fa61017fbc8551266887a6d71016aa41 + "@types/d3-selection": ^2 + checksum: 5ff6212df0d52e20a36e0a102674fb2020d5e3610bd80d75b47394eda0f7cfc6d033004e22b928646d0c999fb756cfab75cc8a73fb5653db9bc2994474d0a65f languageName: node linkType: hard -"@types/d3-dsv@npm:*": - version: 1.0.36 - resolution: "@types/d3-dsv@npm:1.0.36" - checksum: 600e5fd483fc89a4e93f3c3cc2e2b45af0db2b0dc0260a474478da80259c7d63d60c59a08b951a9406383f52263b9a303a7a10c067818805a6e1b97ff3259414 +"@types/d3-dsv@npm:^2": + version: 2.0.3 + resolution: "@types/d3-dsv@npm:2.0.3" + checksum: 82b8aa6409643dbc624e0798a18e16f94aa4676f8e3949e0dc7992d898855e1d6230416c3fbf6d3d3314252e4027176525487dc030abb15031ef8db5b2c30f7a languageName: node linkType: hard -"@types/d3-ease@npm:*": - version: 1.0.9 - resolution: "@types/d3-ease@npm:1.0.9" - checksum: a04fae98985f93886e8a1c7aed77afdd46500fc4877b77d18e2b81206edc6094424d462c3d7e7e275cee0b32cfee1961a674478bb58af025def555f5c2dee68b +"@types/d3-ease@npm:^2": + version: 2.0.2 + resolution: "@types/d3-ease@npm:2.0.2" + checksum: 1c6bb9111129168657c534383651a142466c7af1bfe24d7ae6a064c4a1cf54d151c3df1a7579cec608c016daa2c9278f8a8bad895dcf1f5390a7d2e90de83aaa languageName: node linkType: hard -"@types/d3-fetch@npm:*": - version: 1.1.5 - resolution: "@types/d3-fetch@npm:1.1.5" +"@types/d3-fetch@npm:^2": + version: 2.0.2 + resolution: "@types/d3-fetch@npm:2.0.2" dependencies: - "@types/d3-dsv": "*" - checksum: 056fff1a2f5fa2c4a1293acc6cbe41f9a046b984f2d07203e96138386c5463e7bfc61982fb959889717a422eafbdf512e011fad79f5f96487aaebb9b9c1b0e34 + "@types/d3-dsv": ^2 + checksum: e2bb4f4ad60f2e5569b7d6ef0c980a3c3e5dff5b20c412d89449bbe8449d5babb66f3592f3ff2ba6aad3805a9509de56167c601f941ae528b925aa28900302b7 languageName: node linkType: hard -"@types/d3-force@npm:*": - version: 1.2.1 - resolution: "@types/d3-force@npm:1.2.1" - checksum: 0a000bfa85895359b1a1c6a3526607ce7efdbf8963a3231b960dc24493c6b62804c1f5cb3494e53a7d0eb0a31154586b22fa67710a1a32aab6ca82c8ef08d73d +"@types/d3-force@npm:^2": + version: 2.1.4 + resolution: "@types/d3-force@npm:2.1.4" + checksum: 635a070c68f7ed9ad3962ec7d2fca7c590349648a1b80e6e1a6084885962f84bcaf12b6998fb3aa190fe97b5d8ed2669084145dcd6b89fec6624a5a2d157beb6 languageName: node linkType: hard -"@types/d3-format@npm:*": - version: 1.3.1 - resolution: "@types/d3-format@npm:1.3.1" - checksum: fe7a4f70d4008ef90e71d92e15cb2d260014d2647a9db881c9d0602419f0120ebffa714f44b7c84951418105eb6def271547002eca6e44b0c20da28ba13f5e93 +"@types/d3-format@npm:^2": + version: 2.0.2 + resolution: "@types/d3-format@npm:2.0.2" + checksum: 592a57f880915754f1e14d4113ff231e80c1854dd8186f3c7fb5a7c094db31c52b3727bc0ad83d1fe4297bc5ec2209be34efaa588f72700c1a133b603d168f3b languageName: node linkType: hard -"@types/d3-geo@npm:*": - version: 1.11.1 - resolution: "@types/d3-geo@npm:1.11.1" +"@types/d3-geo@npm:^2": + version: 2.0.4 + resolution: "@types/d3-geo@npm:2.0.4" dependencies: "@types/geojson": "*" - checksum: 032fb87bb2b0bd6edb689626cd70c12e3771bd033f9f9c1ff199974b04ba19449273e8a8b28830950cdbd57db7d9aae767f00d6567c1bfb09c5552eee4f62cf5 + checksum: 80aa1c69479a9af1e2279f202b0ec4fbff30fa88b19bd5d63409b3af80210d468eeabfd4fbe20ed6033dd631928ed77120fa4ad782efee736d2642d41a0e3def languageName: node linkType: hard -"@types/d3-hierarchy@npm:*": - version: 1.1.6 - resolution: "@types/d3-hierarchy@npm:1.1.6" - checksum: 49e53e435ec26253baa138a2622865ade8404a1270333c27e0ec9a1dbd05b863a8cad59fbd80e1e15e5637d6678cc849b154cf27426c93fb510e812dba36335a +"@types/d3-hierarchy@npm:^2": + version: 2.0.2 + resolution: "@types/d3-hierarchy@npm:2.0.2" + checksum: ecac60b196e6b88f729e0a98f8fbba34fc580d3d83869d5498233e83b3cfe74de7640272f3d4d987a51d07c58cc789c8b63b11813114532cdd219f41ddf89154 languageName: node linkType: hard -"@types/d3-interpolate@npm:*": - version: 1.3.1 - resolution: "@types/d3-interpolate@npm:1.3.1" +"@types/d3-interpolate@npm:^2": + version: 2.0.2 + resolution: "@types/d3-interpolate@npm:2.0.2" dependencies: - "@types/d3-color": "*" - checksum: 91328384218d30ddd77d09e97b0e6ad6a92e86e17b56b6ab1b2e6b8aa3c77118f2eb3a1c85ca3a3421935e72a6e43386d21ad4a648a6ae77496a57c12efbff38 + "@types/d3-color": ^2 + checksum: 78c47193da3c114a7d78580c6f8d9915f11df92ce78fe08d13052cf49fab91dcdca938895a778cdc6f9820ebf16df3e0e339c17491a8c2b1140cdd2e09553084 languageName: node linkType: hard -"@types/d3-path@npm:*": - version: 1.0.8 - resolution: "@types/d3-path@npm:1.0.8" - checksum: 3a71cfaf855bd75a7c1d8ad3ed627ef9f91f5aa3a2496fb0fdda0878c67fbf8c520f7ff46fdbee6ca89c776996a52092eea340c07c4c98cee6f4cd93360d7ac5 +"@types/d3-path@npm:^2": + version: 2.0.2 + resolution: "@types/d3-path@npm:2.0.2" + checksum: 2ab49cc87b9d2cb90c189bedf5f0fdc2b1609c3c668664dc76c679054b4bb1bcfaf44e7836e1f7d0b38102cecc269a6c52a353e0ba238c992509cb0e9d6c5c33 languageName: node linkType: hard -"@types/d3-polygon@npm:*": - version: 1.0.7 - resolution: "@types/d3-polygon@npm:1.0.7" - checksum: d1e67a97b2472d8995b42652e7c4c344f84564dbc76356375ef0ed61c904c76756b5acb25fc115ce4835b5c1e602759026b099be7dba82957279a56fc434b6cb +"@types/d3-polygon@npm:^2": + version: 2.0.1 + resolution: "@types/d3-polygon@npm:2.0.1" + checksum: 70cc611b89b5dfe457bf33c2e928cc89bd94900c3f0fd0fc36f7f56d29d94d9f9560f5a55703a1cd7e80f9c6692976a85d81d4b5711658ccd3ae910714b7dfc0 languageName: node linkType: hard -"@types/d3-quadtree@npm:*": - version: 1.0.7 - resolution: "@types/d3-quadtree@npm:1.0.7" - checksum: 29fc60f88c9ca00255bfceda2ea613b4b7051d5e0886d09cf311342771f8bb1379caa1410bd148a2f5900ef334ba30d69df38bd76797667b4a3d5b96e0dde4b8 +"@types/d3-quadtree@npm:^2": + version: 2.0.2 + resolution: "@types/d3-quadtree@npm:2.0.2" + checksum: a6f611df12e1010ebccb891f1080537d3bfb01ae9fe5143d6c20c17881c5db1312c37758ffc03ecf8709ce980feb573c9b32a9a48265c712138e1fce3da6260b languageName: node linkType: hard -"@types/d3-random@npm:*": - version: 1.1.2 - resolution: "@types/d3-random@npm:1.1.2" - checksum: dbfc03e7e6c6888321d761a6490d09a731cd391e185de2b7b5e309834dc9456b7d1a01283c064e4919954bf261db4bcbbddc318b736603e0fc17c96aef3a2baa +"@types/d3-random@npm:^2": + version: 2.2.1 + resolution: "@types/d3-random@npm:2.2.1" + checksum: ea4df7b9e1cdee94d4e28a1e4923a22f5a6c9ac8b77d284d7c0aa7def40a7aab8dbdf81fee0752f5300580d24bca9034ec7a5a74aa5fced5a2a77aadc1b1583e languageName: node linkType: hard -"@types/d3-scale-chromatic@npm:*": - version: 1.5.0 - resolution: "@types/d3-scale-chromatic@npm:1.5.0" - checksum: 45ab6a75fbdb39974c74aac0fcd4c5288e6e16f7789ddc9f9f34be04c66cc454fbe8e678aafe0b1860f6f9ed05a0e9b2f15e58cb4ac4d3add8dee2cb3f53bb03 +"@types/d3-scale-chromatic@npm:^2": + version: 2.0.1 + resolution: "@types/d3-scale-chromatic@npm:2.0.1" + checksum: 5c441309b4d226f51cf86b6adf6f5543067bd6f8adfa94024c5476e355b13b53571a9b52025fdc2713ad37e9146ebaea12a7d089f478dae55cf03797f5d212be languageName: node linkType: hard -"@types/d3-scale@npm:*": - version: 2.2.0 - resolution: "@types/d3-scale@npm:2.2.0" +"@types/d3-scale@npm:^3": + version: 3.3.2 + resolution: "@types/d3-scale@npm:3.3.2" dependencies: - "@types/d3-time": "*" - checksum: 988c3f0e779a25c0d1df41e960a6edda88da05f2b5596a80eb0b7c68981c7e5ee10d665ea76bda44ee2b4eece3a43c2bb0efa059c1accc3fc7f3a4d28761128f + "@types/d3-time": ^2 + checksum: 65dbf85f07a4d6ac26396075b0faa1930cfebb96dc248629d4b82c22457c89161d0f070f9a5554adccee80b959e2c6d7c1ef6b7355743afe91050d71014fe3cf languageName: node linkType: hard -"@types/d3-selection@npm:*": - version: 1.4.2 - resolution: "@types/d3-selection@npm:1.4.2" - checksum: 0f78c09472e77d0d9c64728c6d2e1cbf92e1fcd6011c601cb7138003e48c838d6bb7668dac721039c0e27b8dcf51cbac76cabeb9917ac32c44a72495ebe6c6ea +"@types/d3-selection@npm:^2": + version: 2.0.1 + resolution: "@types/d3-selection@npm:2.0.1" + checksum: 23a337564e4540e1672103ad4d8b8eca1a8c50ec5d3382fbd764a3b93f591b7651441da0ae68119945789a8ba7b8d3ab208088ebf8b6fd1add2134df937bfe15 languageName: node linkType: hard -"@types/d3-shape@npm:*": - version: 1.3.2 - resolution: "@types/d3-shape@npm:1.3.2" +"@types/d3-shape@npm:^2": + version: 2.1.3 + resolution: "@types/d3-shape@npm:2.1.3" dependencies: - "@types/d3-path": "*" - checksum: 635f620253688d5c6c9de0f06f2d22607848a2f4e0671939d7775b1538d08f201dc33c90a8c6636f18ef89aa4db878ff3e7347b23cf85c85df2f0d81179c525e + "@types/d3-path": ^2 + checksum: d0855a1e2c11a4ab23367c86ef0cc104e12bf216f2c007fa5955da7179b60b0426d0e9ddbbbdf93d4342e7dd24c7bcfc3a2bc6258744e03fc44ca460a063dcc3 languageName: node linkType: hard -"@types/d3-time-format@npm:*": - version: 2.1.1 - resolution: "@types/d3-time-format@npm:2.1.1" - checksum: 133b4f2098f8bd7665abd883fd4903cdcc4f28ecd2478512f716e6eafe3842459d5fbdfb900610f50cd4bd3306d466cb61b01998c217ecb04ac251532f073774 +"@types/d3-time-format@npm:^3": + version: 3.0.1 + resolution: "@types/d3-time-format@npm:3.0.1" + checksum: 9ec9156a6facb3e347db3b438938eaac5775a711916fe3667c883431df9b7bcf5d8fcbca7f538b7f0775d8b092c9cf18fe9c0deb7b1a9aa97fb675382a94c88b languageName: node linkType: hard -"@types/d3-time@npm:*": - version: 1.0.10 - resolution: "@types/d3-time@npm:1.0.10" - checksum: 1545fe4544c002755ef7c7a7bb1124bd1cce33718bfb33b7a54447bc072de4bcd62de0453c2a5691075214a890f6c0181977e29b53048eadf42096de2859fb27 +"@types/d3-time@npm:^2": + version: 2.1.1 + resolution: "@types/d3-time@npm:2.1.1" + checksum: 115048d0cd312a3172ef7c03615dfbdbd8b92a93fd7b6d9ca93c49c704fcdb9575f4c57955eb54eb757b9834acaaf47fc52eae103d06246c59ae120de4559cbc languageName: node linkType: hard -"@types/d3-timer@npm:*": - version: 1.0.9 - resolution: "@types/d3-timer@npm:1.0.9" - checksum: a9c73e9e3da06abf818e2f3436c52447e30578a4663e62f0019a4c69caae612ad363025ba9f1ab66125b20621f417ceb34660a635fd2be87c57fa77bba0c83dc +"@types/d3-timer@npm:^2": + version: 2.0.1 + resolution: "@types/d3-timer@npm:2.0.1" + checksum: e59d5ef08c56d570b91e0a10052578e667510d32517f7c4b529b11c979ce984dfd550423d64a0518f2d9a17795d5857653e2e63571804b1eb94e56ff3dfa5261 languageName: node linkType: hard -"@types/d3-transition@npm:*": - version: 1.1.6 - resolution: "@types/d3-transition@npm:1.1.6" +"@types/d3-transition@npm:^2": + version: 2.0.2 + resolution: "@types/d3-transition@npm:2.0.2" dependencies: - "@types/d3-selection": "*" - checksum: 9f3ca6e5e08198382dc193b11094ddd21943ef8571749530ab7d8911a2f30f6e6a4c3c0510529e801c0663ad3b4ffcc505df1a551df37ed5c9098cd36aedef2b + "@types/d3-selection": ^2 + checksum: 1481601c6d6e09d5b936d765f57ba0e29a8fc368ddb6ced9c0df8f4ff7360368d6c49b9022ebfb1ed06745bef0b09623ca6181551569fe82d821359324a1e1f7 languageName: node linkType: hard -"@types/d3-voronoi@npm:*": - version: 1.1.9 - resolution: "@types/d3-voronoi@npm:1.1.9" - checksum: 604e8c4fb4d65f9fc6a549fc3a32a8e9bc7e54e566a2ca6a834f5ff91e41728b3289cfc5c85dd96283fac772c7b9184e023702ec7230e2646a97b78907ddbecb - languageName: node - linkType: hard - -"@types/d3-zoom@npm:*": - version: 1.7.4 - resolution: "@types/d3-zoom@npm:1.7.4" - dependencies: - "@types/d3-interpolate": "*" - "@types/d3-selection": "*" - checksum: 1d81ee12e1cbabe3448e4885168fcad09714b890bd642fa0f318889e377204a05eac4f788020f874424c32ca2f092c8682dae03f579fc173b9cb8f52c5f14bdb - languageName: node - linkType: hard - -"@types/d3@npm:^5.7.2": - version: 5.7.2 - resolution: "@types/d3@npm:5.7.2" - dependencies: - "@types/d3-array": ^1 - "@types/d3-axis": "*" - "@types/d3-brush": "*" - "@types/d3-chord": "*" - "@types/d3-collection": "*" - "@types/d3-color": "*" - "@types/d3-contour": "*" - "@types/d3-dispatch": "*" - "@types/d3-drag": "*" - "@types/d3-dsv": "*" - "@types/d3-ease": "*" - "@types/d3-fetch": "*" - "@types/d3-force": "*" - "@types/d3-format": "*" - "@types/d3-geo": "*" - "@types/d3-hierarchy": "*" - "@types/d3-interpolate": "*" - "@types/d3-path": "*" - "@types/d3-polygon": "*" - "@types/d3-quadtree": "*" - "@types/d3-random": "*" - "@types/d3-scale": "*" - "@types/d3-scale-chromatic": "*" - "@types/d3-selection": "*" - "@types/d3-shape": "*" - "@types/d3-time": "*" - "@types/d3-time-format": "*" - "@types/d3-timer": "*" - "@types/d3-transition": "*" - "@types/d3-voronoi": "*" - "@types/d3-zoom": "*" - checksum: 64609ecb792df8cb556b36f30d0e729762f86b39aac56b346e8f9d08a67b2daa4ff0c83763e228c540b29e51ee005d31adc5be2c0ce4da70167c2bc901640207 +"@types/d3-zoom@npm:^2": + version: 2.0.4 + resolution: "@types/d3-zoom@npm:2.0.4" + dependencies: + "@types/d3-interpolate": ^2 + "@types/d3-selection": ^2 + checksum: 4bbaa987ffd0f0daa69df1c88ab2d1eac7c7f89a301790be18a08a6e66c22b280967a5a6be4a5ab6a60d8ff346a3f13d300be6dc258fa38265c1c48c948853ad + languageName: node + linkType: hard + +"@types/d3@npm:^6.7.0": + version: 6.7.5 + resolution: "@types/d3@npm:6.7.5" + dependencies: + "@types/d3-array": ^2 + "@types/d3-axis": ^2 + "@types/d3-brush": ^2 + "@types/d3-chord": ^2 + "@types/d3-color": ^2 + "@types/d3-contour": ^2 + "@types/d3-delaunay": ^5 + "@types/d3-dispatch": ^2 + "@types/d3-drag": ^2 + "@types/d3-dsv": ^2 + "@types/d3-ease": ^2 + "@types/d3-fetch": ^2 + "@types/d3-force": ^2 + "@types/d3-format": ^2 + "@types/d3-geo": ^2 + "@types/d3-hierarchy": ^2 + "@types/d3-interpolate": ^2 + "@types/d3-path": ^2 + "@types/d3-polygon": ^2 + "@types/d3-quadtree": ^2 + "@types/d3-random": ^2 + "@types/d3-scale": ^3 + "@types/d3-scale-chromatic": ^2 + "@types/d3-selection": ^2 + "@types/d3-shape": ^2 + "@types/d3-time": ^2 + "@types/d3-time-format": ^3 + "@types/d3-timer": ^2 + "@types/d3-transition": ^2 + "@types/d3-zoom": ^2 + checksum: b800c9d251d265c94dc3b91f0dd060786dd3366a56a159b8df86ad2cb258a6ed92eb1c13f267f22b522eda7b04684adaa6a5989fb3543cef32dcaedde389bec5 languageName: node linkType: hard @@ -2574,6 +2584,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.3.12": + version: 7.5.0 + resolution: "@types/semver@npm:7.5.0" + checksum: 0a64b9b9c7424d9a467658b18dd70d1d781c2d6f033096a6e05762d20ebbad23c1b69b0083b0484722aabf35640b78ccc3de26368bcae1129c87e9df028a22e2 + languageName: node + linkType: hard + "@types/semver@npm:^7.3.3, @types/semver@npm:^7.3.4": version: 7.3.4 resolution: "@types/semver@npm:7.3.4" @@ -2712,18 +2729,19 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/eslint-plugin@npm:5.16.0" +"@typescript-eslint/eslint-plugin@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/eslint-plugin@npm:5.60.1" dependencies: - "@typescript-eslint/scope-manager": 5.16.0 - "@typescript-eslint/type-utils": 5.16.0 - "@typescript-eslint/utils": 5.16.0 - debug: ^4.3.2 - functional-red-black-tree: ^1.0.1 - ignore: ^5.1.8 - regexpp: ^3.2.0 - semver: ^7.3.5 + "@eslint-community/regexpp": ^4.4.0 + "@typescript-eslint/scope-manager": 5.60.1 + "@typescript-eslint/type-utils": 5.60.1 + "@typescript-eslint/utils": 5.60.1 + debug: ^4.3.4 + grapheme-splitter: ^1.0.4 + ignore: ^5.2.0 + natural-compare-lite: ^1.4.0 + semver: ^7.3.7 tsutils: ^3.21.0 peerDependencies: "@typescript-eslint/parser": ^5.0.0 @@ -2731,24 +2749,24 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 4007cc1599503424037300e7401fb969ca441b122ef8a8f2fc8d70f84d656fdf7ab7b0d00e506a3aaf702871616c3756da17eb1508ff315dfb25170f2d28a904 + checksum: 6ea3fdc64b216ee709318bfce1573cd8d90836150f0075aaa8755c347541af9ec026043e538a3264d28d1b32ff49b1fd7c6163826b8513f19f0957fefccf7752 languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/parser@npm:5.16.0" +"@typescript-eslint/parser@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/parser@npm:5.60.1" dependencies: - "@typescript-eslint/scope-manager": 5.16.0 - "@typescript-eslint/types": 5.16.0 - "@typescript-eslint/typescript-estree": 5.16.0 - debug: ^4.3.2 + "@typescript-eslint/scope-manager": 5.60.1 + "@typescript-eslint/types": 5.60.1 + "@typescript-eslint/typescript-estree": 5.60.1 + debug: ^4.3.4 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 40006578e9ac451c80dc4b4b7e29af97b53fb9e9ea660d6ca17fb98b5c9858c648f9b17523c9de9b9b9e4155af17b65435e6163f02c4a2dfacf48274f45cba21 + checksum: 08f1552ab0da178524a8de3654d2fb7c8ecb9efdad8e771c9cbf4af555c42e77d17b2c182d139a531cc76c3cabd091d1d25024c2c215cb809dca8b147c8a493c languageName: node linkType: hard @@ -2762,19 +2780,30 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/type-utils@npm:5.16.0" +"@typescript-eslint/scope-manager@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/scope-manager@npm:5.60.1" dependencies: - "@typescript-eslint/utils": 5.16.0 - debug: ^4.3.2 + "@typescript-eslint/types": 5.60.1 + "@typescript-eslint/visitor-keys": 5.60.1 + checksum: 32c0786123f12fbb861aba3527471134a2e9978c7f712e0d7650080651870903482aed72a55f81deba9493118c1ca3c57edaaaa75d7acd9892818e3e9cc341ef + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/type-utils@npm:5.60.1" + dependencies: + "@typescript-eslint/typescript-estree": 5.60.1 + "@typescript-eslint/utils": 5.60.1 + debug: ^4.3.4 tsutils: ^3.21.0 peerDependencies: eslint: "*" peerDependenciesMeta: typescript: optional: true - checksum: 86d9f1dff6a096c8465453b8c7d0cc667b87a769f19073bfa9bbd36f8baa772c0384ec396b1132052383846bbbcf0d051345ed7d373260c1b506ed27100b383d + checksum: f8d5f87b5441d5c671f69631efd103f5f45e0cb7dbe0131a5b4234a5208ac845041219e8baaa3adc341e82a602165dd6fabf4fd06964d0109d0875425c8ac918 languageName: node linkType: hard @@ -2785,6 +2814,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/types@npm:5.60.1" + checksum: 766b6c857493b72a8f515e6a8e409476a317b7a7f0401fbcdf18f417839fca004dcaf06f58eb5ba00777e3ca9c68cd2f56fda79f3a8eb8a418095b5b1f625712 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.16.0": version: 5.16.0 resolution: "@typescript-eslint/typescript-estree@npm:5.16.0" @@ -2803,7 +2839,43 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.16.0, @typescript-eslint/utils@npm:^5.10.0": +"@typescript-eslint/typescript-estree@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/typescript-estree@npm:5.60.1" + dependencies: + "@typescript-eslint/types": 5.60.1 + "@typescript-eslint/visitor-keys": 5.60.1 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + semver: ^7.3.7 + tsutils: ^3.21.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 5bb9d08c3cbc303fc64647878cae37283c4cfa9e3ed00da02ee25dc2e46798a1ad6964c9f04086f0134716671357e6569a65ea0ae75f0f3ff94ae67666385c6f + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/utils@npm:5.60.1" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@types/json-schema": ^7.0.9 + "@types/semver": ^7.3.12 + "@typescript-eslint/scope-manager": 5.60.1 + "@typescript-eslint/types": 5.60.1 + "@typescript-eslint/typescript-estree": 5.60.1 + eslint-scope: ^5.1.1 + semver: ^7.3.7 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 00c1adaa09d5d5be947e98962a78c21ed08c3ac46dd5ddd7b78f6102537d50afd4578a42a3e09a24dd51f5bc493f0b968627b4423647540164b2d2380afa9246 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:^5.10.0": version: 5.16.0 resolution: "@typescript-eslint/utils@npm:5.16.0" dependencies: @@ -2829,6 +2901,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:5.60.1": + version: 5.60.1 + resolution: "@typescript-eslint/visitor-keys@npm:5.60.1" + dependencies: + "@typescript-eslint/types": 5.60.1 + eslint-visitor-keys: ^3.3.0 + checksum: 137f6a6f8efb398969087147b59f99f7d0deed044d89d7efce3631bb90bc32e3a13a5cee6a65e1c9830862c5c4402ac1a9b2c9e31fe46d1716602af2813bffae + languageName: node + linkType: hard + "@xmldom/xmldom@npm:^0.7.2": version: 0.7.9 resolution: "@xmldom/xmldom@npm:0.7.9" @@ -4250,86 +4332,89 @@ __metadata: languageName: node linkType: hard -"d3-array@npm:1, d3-array@npm:^1.1.1, d3-array@npm:^1.2.0": - version: 1.2.4 - resolution: "d3-array@npm:1.2.4" - checksum: d0be1fa7d72dbfac8a3bcffbb669d42bcb9128d8818d84d2b1df0c60bbe4c8e54a798be0457c55a219b399e2c2fabcbd581cbb130eb638b5436b0618d7e56000 +"d3-array@npm:2, d3-array@npm:^2.3.0, d3-array@npm:^2.5.0": + version: 2.12.1 + resolution: "d3-array@npm:2.12.1" + dependencies: + internmap: ^1.0.0 + checksum: 97853b7b523aded17078f37c67742f45d81e88dda2107ae9994c31b9e36c5fa5556c4c4cf39650436f247813602dfe31bf7ad067ff80f127a16903827f10c6eb languageName: node linkType: hard -"d3-axis@npm:1": - version: 1.0.12 - resolution: "d3-axis@npm:1.0.12" - checksum: b1cf820fb6e95cc3371b340353b05272dba16ce6ad4fe9a0992d075ab48a08810f87f5e6c7cbb6c63fca1ee1e9b7c822307a1590187daa7627f45728a747c746 +"d3-axis@npm:2": + version: 2.1.0 + resolution: "d3-axis@npm:2.1.0" + checksum: 43d80f68e516b315bbe86afff1552abd9518296e60f58a9a70ee9a524c109dec9d4585da5fc34a5c589e599c5ee4fe465f850c5a43f5e112c3850965bb00e9f5 languageName: node linkType: hard -"d3-brush@npm:1": - version: 1.1.5 - resolution: "d3-brush@npm:1.1.5" +"d3-brush@npm:2": + version: 2.1.0 + resolution: "d3-brush@npm:2.1.0" dependencies: - d3-dispatch: 1 - d3-drag: 1 - d3-interpolate: 1 - d3-selection: 1 - d3-transition: 1 - checksum: 666cd89eb6e5677325f835cf65ff2834d2d75d142a73c3f30cfae762caf7fd6280ba7ba50cc8520d2bbf59c63ebcbaf724b596c027884efa46360a0c85e2ee35 + d3-dispatch: 1 - 2 + d3-drag: 2 + d3-interpolate: 1 - 2 + d3-selection: 2 + d3-transition: 2 + checksum: 35fb7e3b422626194aac1b85434e6a346de1b69f497d1f80d2f6bf881602e8826255a7c95a0c27f0de5ec2f5f7bc8481d6e1df24c80ebb28175a0830c868d218 languageName: node linkType: hard -"d3-chord@npm:1": - version: 1.0.6 - resolution: "d3-chord@npm:1.0.6" +"d3-chord@npm:2": + version: 2.0.0 + resolution: "d3-chord@npm:2.0.0" dependencies: - d3-array: 1 - d3-path: 1 - checksum: e4ca95ffff089f0eccf796d16a5574121e0ecbe658dcd9d5fa760af3573c3349264ce325c0adf1f32bcad67038d3938edd109712166cfb5b3bbe068e27c012e9 + d3-path: 1 - 2 + checksum: 932f1e9a50a68b95f42431fe043164c671433226582cf4e5f5a8f7064da99efaca656283861bf243286c5ef7dcf87aa77811418564fba83350da48d8c50074c0 languageName: node linkType: hard -"d3-collection@npm:1": - version: 1.0.7 - resolution: "d3-collection@npm:1.0.7" - checksum: 9c6b910a9da0efb021e294509f98263ca4f62d10b997bb30ccfb6edd582b703da36e176b968b5bac815fbb0f328e49643c38cf93b5edf8572a179ba55cf4a09d +"d3-color@npm:1 - 2, d3-color@npm:2": + version: 2.0.0 + resolution: "d3-color@npm:2.0.0" + checksum: b887354aa383937abd04fbffed3e26e5d6a788472cd3737fb10735930e427763e69fe93398663bccf88c0b53ee3e638ac6fcf0c02226b00ed9e4327c2dfbf3dc languageName: node linkType: hard -"d3-color@npm:1": - version: 1.4.1 - resolution: "d3-color@npm:1.4.1" - checksum: a214b61458b5fcb7ad1a84faed0e02918037bab6be37f2d437bf0e2915cbd854d89fbf93754f17b0781c89e39d46704633d05a2bfae77e6209f0f4b140f9894b +"d3-contour@npm:2": + version: 2.0.0 + resolution: "d3-contour@npm:2.0.0" + dependencies: + d3-array: 2 + checksum: 7d46bad378f6e329dddcc52df76077f28b563219cf5a0003385fbcb6c501d5fbb10d71c508da6a7690b527ccdbc1909b6f1f36fa36f03b43b19f8bcab0c2961d languageName: node linkType: hard -"d3-contour@npm:1": - version: 1.3.2 - resolution: "d3-contour@npm:1.3.2" +"d3-delaunay@npm:5": + version: 5.3.0 + resolution: "d3-delaunay@npm:5.3.0" dependencies: - d3-array: ^1.1.1 - checksum: c18a099a7f4af2adf788e96d07bfc7236661a6e40c017ef8e172fe0142561f3722f71263075c565a17b72e6cd6a2a05de3868fcc5420eb77b00d3a0179a69a0d + delaunator: 4 + checksum: 3fa5ae167eb86e62ca0f9c3e8d05470b23572b4b480f05201705c0db976d403834cee1cdf264a41c97e45238e3999d48cc593f97d0da37229a42673a6bb10e95 languageName: node linkType: hard -"d3-dispatch@npm:1": - version: 1.0.6 - resolution: "d3-dispatch@npm:1.0.6" - checksum: b4ecb016b6dda8b99aa4263b2d0a0c7b12e7dea93e4b0ce3013c94dca4d360d9ba00f5bdc15dc944cc4543af8e341067bd628f061f7b8deb642257e2ac90d06c +"d3-dispatch@npm:1 - 2, d3-dispatch@npm:2": + version: 2.0.0 + resolution: "d3-dispatch@npm:2.0.0" + checksum: cf473676ae0df1915d51d056d2c6734ceec480d258611d970a01847c50e8c273c185032bf9ed491abd077696bcbeeb491dc94af53e888871f3a1a0fac7365cec languageName: node linkType: hard -"d3-drag@npm:1": - version: 1.2.5 - resolution: "d3-drag@npm:1.2.5" +"d3-drag@npm:2": + version: 2.0.0 + resolution: "d3-drag@npm:2.0.0" dependencies: - d3-dispatch: 1 - d3-selection: 1 - checksum: 6e86e89aa8d511979eea1b5326709c05c2a3c2d43a93a82ed6b6f98528b2ab03b2f58f5e4f66582f2f1c0ae44f9c19f6f4f857249eb66aabc46e4942295fa0a7 + d3-dispatch: 1 - 2 + d3-selection: 2 + checksum: 47f0bcdd097fd363d59da41299f276c1f6bd88bc460f53871b3d9c35982d40f59a535c6de7fa07a51120e56c99064bd9aa3551279dc09ce4204c639d54f80399 languageName: node linkType: hard -"d3-dsv@npm:1": - version: 1.2.0 - resolution: "d3-dsv@npm:1.2.0" +"d3-dsv@npm:1 - 2, d3-dsv@npm:2": + version: 2.0.0 + resolution: "d3-dsv@npm:2.0.0" dependencies: commander: 2 iconv-lite: 0.4 @@ -4344,231 +4429,224 @@ __metadata: json2tsv: bin/json2dsv tsv2csv: bin/dsv2dsv tsv2json: bin/dsv2json - checksum: 96c6e3d5ca1566624ca613b5941bc6fa916082cbe4b2b71cb6c5978c471db58c489b17206e3e31fbe30719dbd75e9c8ed8ab12a9d353cff90a35102690de7823 + checksum: 01b12d81e4ca3996f2e921388b1929c358a39711bf250f2c53dd0e452b80465ebe31ddb58a4064f160322dec7aaf2ceae1a249874af989404705fcfdf1e9b64d languageName: node linkType: hard -"d3-ease@npm:1": - version: 1.0.6 - resolution: "d3-ease@npm:1.0.6" - checksum: 959a4df80c2c6a0734bcfb1eb363bef92deb39f9b890e2a539ed4d923ab75093959c5aad9f74de0450b9039b8375ae89d3eba665d46d30520350ca35cabf40b7 +"d3-ease@npm:1 - 2, d3-ease@npm:2": + version: 2.0.0 + resolution: "d3-ease@npm:2.0.0" + checksum: 1a9f6dfc836f0c66fba1ed28f0a3ad170d7c4f4812d442c6b562163e1a60283cc697e72a2cc4ba64abff9e77ad56354847986a5964e0c661af9b6d132c642e29 languageName: node linkType: hard -"d3-fetch@npm:1": - version: 1.2.0 - resolution: "d3-fetch@npm:1.2.0" +"d3-fetch@npm:2": + version: 2.0.0 + resolution: "d3-fetch@npm:2.0.0" dependencies: - d3-dsv: 1 - checksum: 00f091945bff4afbd06e6ce9ad762f0e91b7aac912c1ae7fe0efdbcce3a997d4fa2a93c254a3ba9b3f53f2134d606b20fb13791adbf5c6ed5c0be329a775945f + d3-dsv: 1 - 2 + checksum: e592420726c39dbbe342504761e5c80a026fc9add4f808ed4e01ab66c3064f7251f988dd2bcb6b8b61f8866769470eabb14a8d7f18e014338d9d9637ffd51af1 languageName: node linkType: hard -"d3-force@npm:1": - version: 1.2.1 - resolution: "d3-force@npm:1.2.1" +"d3-force@npm:2": + version: 2.1.1 + resolution: "d3-force@npm:2.1.1" dependencies: - d3-collection: 1 - d3-dispatch: 1 - d3-quadtree: 1 - d3-timer: 1 - checksum: b73fe29d6c9a9c432ae65166d71238d14578a3a9537df095bebff87b7814161cd2822aff54a38d2400edb98b7f6d9221a810dcad7a53c6e8ddff0973f44ab3fa + d3-dispatch: 1 - 2 + d3-quadtree: 1 - 2 + d3-timer: 1 - 2 + checksum: aaee5b86d753450e72dae6748765ac3e0b7b784bd420a61264b778d697b9521a343b74b5c55654be2ff7fdf9bada0953a6fcae9be69091176d0579b56df72937 languageName: node linkType: hard -"d3-format@npm:1": - version: 1.4.4 - resolution: "d3-format@npm:1.4.4" - checksum: 412d3c6299dc2ccca89544c27d7027d6f5cc682ce72f107bceba36b76cdfe88071b9fa205367422df09876135a1f4c05df9a14bab343bf9f99cf4d6291a222e2 +"d3-format@npm:1 - 2, d3-format@npm:2": + version: 2.0.0 + resolution: "d3-format@npm:2.0.0" + checksum: c4d3c8f9941d097d514d3986f54f21434e08e5876dc08d1d65226447e8e167600d5b9210235bb03fd45327225f04f32d6e365f08f76d2f4b8bff81594851aaf7 languageName: node linkType: hard -"d3-geo@npm:1": - version: 1.12.1 - resolution: "d3-geo@npm:1.12.1" +"d3-geo@npm:2": + version: 2.0.2 + resolution: "d3-geo@npm:2.0.2" dependencies: - d3-array: 1 - checksum: 8ede498e5fce65c127403646f5cc6181a858a1e401e23e2856ce50ad27e6fdf8b49aeb88d2fad02696879d5825a45420ca1b5db9fa9c935ee413fe15b5bc37c4 + d3-array: ^2.5.0 + checksum: 992f667c646f8e2ea810de20e62914128e119f0458bce4090934287af3b93395632ed4af16aae7ccae095ae702a23b5d7a49888674f1aa27ab1a6e410882d86c languageName: node linkType: hard -"d3-hierarchy@npm:1": - version: 1.1.9 - resolution: "d3-hierarchy@npm:1.1.9" - checksum: 5fd8761c302252cb9abe9ce2a0934fc97104dd0df8d1b5de6472532903416f40e13b4b58d03ce215a0b816d7129c4ed4503bd4fdbc00a130fdcf46a63d734a52 +"d3-hierarchy@npm:2": + version: 2.0.0 + resolution: "d3-hierarchy@npm:2.0.0" + checksum: 594bea104d3cf947da8499f3c50c93d914c5af34195ef3848e4888ed5c8c1aa6a1adae1dd656acfd04779005255347f68a3d270d825ec94fc88b88a72dd7cb4e languageName: node linkType: hard -"d3-interpolate@npm:1": - version: 1.4.0 - resolution: "d3-interpolate@npm:1.4.0" +"d3-interpolate@npm:1 - 2, d3-interpolate@npm:1.2.0 - 2, d3-interpolate@npm:2": + version: 2.0.1 + resolution: "d3-interpolate@npm:2.0.1" dependencies: - d3-color: 1 - checksum: d98988bd1e2f59d01f100d0a19315ad8f82ef022aa09a65aff76f747a44f9b52f2d64c6578b8f47e01f2b14a8f0ef88f5460d11173c0dd2d58238c217ac0ec03 + d3-color: 1 - 2 + checksum: 4a2018ac34fbcc3e0e7241e117087ca1b2274b8b33673913658623efacc5db013b8d876586d167b23e3145bdb34ec8e441d301299b082e1a90985b2f18d4299c languageName: node linkType: hard -"d3-path@npm:1": - version: 1.0.9 - resolution: "d3-path@npm:1.0.9" - checksum: d4382573baf9509a143f40944baeff9fead136926aed6872f7ead5b3555d68925f8a37935841dd51f1d70b65a294fe35c065b0906fb6e42109295f6598fc16d0 +"d3-path@npm:1 - 2, d3-path@npm:2": + version: 2.0.0 + resolution: "d3-path@npm:2.0.0" + checksum: e39e91dfb9abf9637962caede1f4ea4877f4b9e1c914868bdfc355688e9a637ba51bed0fb6180934eb596e50a4d0d1f001b5f2e98a4a3d23cc42558acfbd1f2c languageName: node linkType: hard -"d3-polygon@npm:1": - version: 1.0.6 - resolution: "d3-polygon@npm:1.0.6" - checksum: 4a9764c2064d15e9f4fc9018c975f127540f6e701c18442e2a2e9339e743726f40e017d5213982d983cac3c23802321c257f2a10e686c803ec5533c6ff42bb7a +"d3-polygon@npm:2": + version: 2.0.0 + resolution: "d3-polygon@npm:2.0.0" + checksum: aeabedd8c74b0087d9b3fa9d9a95ce6535edb07c546cb070ffb1f971a3e9112124a9f63bf1377cbb6889d2cb0268363c4a864ec8c7629d990623fff73262d1ea languageName: node linkType: hard -"d3-quadtree@npm:1": - version: 1.0.7 - resolution: "d3-quadtree@npm:1.0.7" - checksum: 32181f578cbd69eed6b240073fed7f977f8039a121a3b9fc58ea1eea0c3c14d1237ef48cb4f80abb833063f8b0e7b885ef6de734e7bcc4e5b37e53ec444830f8 +"d3-quadtree@npm:1 - 2, d3-quadtree@npm:2": + version: 2.0.0 + resolution: "d3-quadtree@npm:2.0.0" + checksum: e5f9cee19a636666e9f1614f9a9508dde9af47d80769ecb70b6b5033448a8c3ae96f39f1ffea0d1782442559412e3f98508fedf5dc39fe09a2f5995e6a0913bf languageName: node linkType: hard -"d3-random@npm:1": - version: 1.1.2 - resolution: "d3-random@npm:1.1.2" - checksum: a27326319fa61d59b6ce8d5ce7547cc823dee1bc6dda35e9c233d709f43f76488c09353862463c9c5da99081482b0f7ea4177d78721b67bb677bb12354bffe42 +"d3-random@npm:2": + version: 2.2.2 + resolution: "d3-random@npm:2.2.2" + checksum: 79931d642f059c874c2be964b629ac0ce0f73306fa744e2ac8eb5ef1592ecfe8ab4a31a5273bef75db7ba2055c344af9921ef1bf55070edd826d7a7ba0b47331 languageName: node linkType: hard -"d3-scale-chromatic@npm:1": - version: 1.5.0 - resolution: "d3-scale-chromatic@npm:1.5.0" +"d3-scale-chromatic@npm:2": + version: 2.0.0 + resolution: "d3-scale-chromatic@npm:2.0.0" dependencies: - d3-color: 1 - d3-interpolate: 1 - checksum: 3bff7717f6e6b309b3347d48d6532e2295037a280bc5174f908ce5fc0e17a9470f6b202e49499b01a17a1f28cb76a61aae870a6c13c57195a362847f33747501 + d3-color: 1 - 2 + d3-interpolate: 1 - 2 + checksum: 9fe5b4c1d9907abbda76e414856d9089182a0641f3bbf43d8d3008dbcccb52781e21793682e2b53663d3c6cd63e76965f961894e53ed3b01a345797412fe5b1f languageName: node linkType: hard -"d3-scale@npm:2": - version: 2.2.2 - resolution: "d3-scale@npm:2.2.2" +"d3-scale@npm:3": + version: 3.3.0 + resolution: "d3-scale@npm:3.3.0" dependencies: - d3-array: ^1.2.0 - d3-collection: 1 - d3-format: 1 - d3-interpolate: 1 - d3-time: 1 - d3-time-format: 2 - checksum: 42086d4b9db9f8492a99dbbdacf546983faef1bb6260fe875c0c1884f1ca9cf5fd233de3702c2f9e24145b1c5383945e929c8682d80fa57ab515ef2c4f2c61f6 + d3-array: ^2.3.0 + d3-format: 1 - 2 + d3-interpolate: 1.2.0 - 2 + d3-time: ^2.1.1 + d3-time-format: 2 - 3 + checksum: f77e73f0fb422292211d0687914c30d26e29011a936ad2a535a868ae92f306c3545af1fe7ea5db1b3e67dbce7a6c6cd952e53d02d1d557543e7e5d30e30e52f2 languageName: node linkType: hard -"d3-selection@npm:1, d3-selection@npm:^1.1.0": - version: 1.4.1 - resolution: "d3-selection@npm:1.4.1" - checksum: b421eab2391e2744736f85f1bbcd9f79c85bfdb8d1dfa4aad614868a49decd213fde5e23d5e40c6b8eff31c3b80e0a162c01a6ccf07ea465058197461856cc9f +"d3-selection@npm:2": + version: 2.0.0 + resolution: "d3-selection@npm:2.0.0" + checksum: c00143f55d510e405fc51e5ef626cb6b3705181ec3d595e84e5cb977508e30228d95d7e209aff6e686ff758c562c5dd17e974325ee8c8555bb6f4f382331ec7d languageName: node linkType: hard -"d3-shape@npm:1": - version: 1.3.7 - resolution: "d3-shape@npm:1.3.7" +"d3-shape@npm:2": + version: 2.1.0 + resolution: "d3-shape@npm:2.1.0" dependencies: - d3-path: 1 - checksum: 46566a3ab64a25023653bf59d64e81e9e6c987e95be985d81c5cedabae5838bd55f4a201a6b69069ca862eb63594cd263cac9034afc2b0e5664dfe286c866129 + d3-path: 1 - 2 + checksum: 4a82a83fbb15aadee3eb6661226a34bcd793cdbcd7aa5bf980a4724efc93eb94acc6c499f0ebedc9c3144c57c0f033867d137f41e86459acbd5d7181cb27b49c languageName: node linkType: hard -"d3-time-format@npm:2": - version: 2.2.3 - resolution: "d3-time-format@npm:2.2.3" +"d3-time-format@npm:2 - 3, d3-time-format@npm:3": + version: 3.0.0 + resolution: "d3-time-format@npm:3.0.0" dependencies: - d3-time: 1 - checksum: 4409360128889d1cb86a0921c2699e33931309ca9fce6f2c66c0850e3e7f0667e86e07fb6f0e2ffc4fb94c20332ea1f7d341572b463988073dc587aabe328d4f - languageName: node - linkType: hard - -"d3-time@npm:1": - version: 1.1.0 - resolution: "d3-time@npm:1.1.0" - checksum: 33fcfff94ff093dde2048c190ecca8b39fe0ec8b3c61e9fc39c5f6072ce5b86dd2b91823f086366995422bbbac7f74fd9abdb7efe4f292a73b1c6197c699cc78 - languageName: node - linkType: hard - -"d3-timer@npm:1": - version: 1.0.10 - resolution: "d3-timer@npm:1.0.10" - checksum: f7040953672deb2dfa03830ace80dbbcb212f80890218eba15dcca6f33f74102d943023ccc2a563295195cd8c63639bb2410ef1691c8fecff4a114fdf5c666f4 + d3-time: 1 - 2 + checksum: c20c1667dbea653f81d923e741f84c23e4b966002ba0d6ed94cbc70692105566e55e89d18d175404534a879383fd1123300bd12885a3c924fe924032bb0060db languageName: node linkType: hard -"d3-transition@npm:1": - version: 1.3.2 - resolution: "d3-transition@npm:1.3.2" +"d3-time@npm:1 - 2, d3-time@npm:2, d3-time@npm:^2.1.1": + version: 2.1.1 + resolution: "d3-time@npm:2.1.1" dependencies: - d3-color: 1 - d3-dispatch: 1 - d3-ease: 1 - d3-interpolate: 1 - d3-selection: ^1.1.0 - d3-timer: 1 - checksum: 1b4a0cfa7aeb4033ab20e26a310488cfac989de44c6c2bf10e9f0808af915a33add6dca23fbafcefe8c08613fd0d6a933e48b4de24c0779163c2852a1c7c16f4 + d3-array: 2 + checksum: d1c7b9658c20646e46c3dd19e11c38e02dec098e8baa7d2cd868af8eb01953668f5da499fa33dc63541cf74a26e788786f8828c4381dbbf475a76b95972979a6 languageName: node linkType: hard -"d3-voronoi@npm:1": - version: 1.1.4 - resolution: "d3-voronoi@npm:1.1.4" - checksum: d28a74bc62f2b936b0d3b51d5be8d2366afca4fd7026d7ee8f655600650bf0c985da38a8c3ae46bfa315b5f524f3ca1c5211437cf1c8c737cc1da681e015baee +"d3-timer@npm:1 - 2, d3-timer@npm:2": + version: 2.0.0 + resolution: "d3-timer@npm:2.0.0" + checksum: 70733c3baffe473155b712896f04f27dae32d6e94169827f57aebb203e190926ba37af12c5f56cbc7126e538a4b1cd083f2451b80dc2a5644d076b6b31982bd8 languageName: node linkType: hard -"d3-zoom@npm:1": - version: 1.8.3 - resolution: "d3-zoom@npm:1.8.3" +"d3-transition@npm:2": + version: 2.0.0 + resolution: "d3-transition@npm:2.0.0" dependencies: - d3-dispatch: 1 - d3-drag: 1 - d3-interpolate: 1 - d3-selection: 1 - d3-transition: 1 - checksum: de408e5dc6df1481ef6854a3d495f8e963dbf5b0de41bcbd35def0602abda55b3f2c1fa751c75c2f0a9bafd3b278f30795c27503fe609b3dbe06a0720d01d5be + d3-color: 1 - 2 + d3-dispatch: 1 - 2 + d3-ease: 1 - 2 + d3-interpolate: 1 - 2 + d3-timer: 1 - 2 + peerDependencies: + d3-selection: 2 + checksum: 8f29d195adc75e20dcffacb44e571fd64a2f60d511e6e7438019356bbc6c3ada10ae2b1393c36242ab71eb1b131f60cf71d0d76bd4cab4fce6a5ddaf1f6f6b00 languageName: node linkType: hard -"d3@npm:^5.16.0": - version: 5.16.0 - resolution: "d3@npm:5.16.0" - dependencies: - d3-array: 1 - d3-axis: 1 - d3-brush: 1 - d3-chord: 1 - d3-collection: 1 - d3-color: 1 - d3-contour: 1 - d3-dispatch: 1 - d3-drag: 1 - d3-dsv: 1 - d3-ease: 1 - d3-fetch: 1 - d3-force: 1 - d3-format: 1 - d3-geo: 1 - d3-hierarchy: 1 - d3-interpolate: 1 - d3-path: 1 - d3-polygon: 1 - d3-quadtree: 1 - d3-random: 1 - d3-scale: 2 - d3-scale-chromatic: 1 - d3-selection: 1 - d3-shape: 1 - d3-time: 1 - d3-time-format: 2 - d3-timer: 1 - d3-transition: 1 - d3-voronoi: 1 - d3-zoom: 1 - checksum: 1462789c421c3ea3930a18b91be6c02c7b976fa4d714200ee2a042c62cbfb349448c79f1ae3dbaf186f79edb734b7aa7b734ee6ad61d81ab4305e6663623ab8e +"d3-zoom@npm:2": + version: 2.0.0 + resolution: "d3-zoom@npm:2.0.0" + dependencies: + d3-dispatch: 1 - 2 + d3-drag: 2 + d3-interpolate: 1 - 2 + d3-selection: 2 + d3-transition: 2 + checksum: d98cc6ffa1105b0062ee312303caff9345ecd1f6df11b7da1e008c1c4731551755ac951327e8c758ffcf74e761218bc6c4f4a6b48f91551ea5d67e0dcf574a49 + languageName: node + linkType: hard + +"d3@npm:^6.7.0": + version: 6.7.0 + resolution: "d3@npm:6.7.0" + dependencies: + d3-array: 2 + d3-axis: 2 + d3-brush: 2 + d3-chord: 2 + d3-color: 2 + d3-contour: 2 + d3-delaunay: 5 + d3-dispatch: 2 + d3-drag: 2 + d3-dsv: 2 + d3-ease: 2 + d3-fetch: 2 + d3-force: 2 + d3-format: 2 + d3-geo: 2 + d3-hierarchy: 2 + d3-interpolate: 2 + d3-path: 2 + d3-polygon: 2 + d3-quadtree: 2 + d3-random: 2 + d3-scale: 3 + d3-scale-chromatic: 2 + d3-selection: 2 + d3-shape: 2 + d3-time: 2 + d3-time-format: 3 + d3-timer: 2 + d3-transition: 2 + d3-zoom: 2 + checksum: 68e37250bacbfaa677d45dbbd82395e898d628d4598b827a3682e3612e0e43c67784f2cd7ecdc23e75c48ad0abe07b65e20c5aa13fb5ebf8879db6db468032e9 languageName: node linkType: hard @@ -4785,6 +4863,13 @@ __metadata: languageName: node linkType: hard +"delaunator@npm:4": + version: 4.0.1 + resolution: "delaunator@npm:4.0.1" + checksum: a49f1c23edbcb79079a13577d32fcd46d0db30879c8484f742a0d840923085f2f3de35a9bfbb96eadd12201ffb7c3adf45b0f528d08b71cb547c5f8068b5d61b + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -6320,7 +6405,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.4": +"globby@npm:^11.0.4, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -6408,6 +6493,13 @@ __metadata: languageName: node linkType: hard +"grapheme-splitter@npm:^1.0.4": + version: 1.0.4 + resolution: "grapheme-splitter@npm:1.0.4" + checksum: 0c22ec54dee1b05cd480f78cf14f732cb5b108edc073572c4ec205df4cd63f30f8db8025afc5debc8835a8ddeacf648a1c7992fe3dcd6ad38f9a476d84906620 + languageName: node + linkType: hard + "graphql@npm:^15.5.1": version: 15.8.0 resolution: "graphql@npm:15.8.0" @@ -6662,7 +6754,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.1.8, ignore@npm:^5.2.0": +"ignore@npm:^5.2.0": version: 5.2.0 resolution: "ignore@npm:5.2.0" checksum: 6b1f926792d614f64c6c83da3a1f9c83f6196c2839aa41e1e32dd7b8d174cef2e329d75caabb62cb61ce9dc432f75e67d07d122a037312db7caa73166a1bdb77 @@ -6797,6 +6889,13 @@ __metadata: languageName: node linkType: hard +"internmap@npm:^1.0.0": + version: 1.0.1 + resolution: "internmap@npm:1.0.1" + checksum: 9d00f8c0cf873a24a53a5a937120dab634c41f383105e066bb318a61864e6292d24eb9516e8e7dccfb4420ec42ca474a0f28ac9a6cc82536898fa09bbbe53813 + languageName: node + linkType: hard + "ip@npm:^1.1.5": version: 1.1.5 resolution: "ip@npm:1.1.5" @@ -8702,6 +8801,13 @@ __metadata: languageName: node linkType: hard +"natural-compare-lite@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare-lite@npm:1.4.0" + checksum: 5222ac3986a2b78dd6069ac62cbb52a7bf8ffc90d972ab76dfe7b01892485d229530ed20d0c62e79a6b363a663b273db3bde195a1358ce9e5f779d4453887225 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -9825,6 +9931,26 @@ __metadata: languageName: node linkType: hard +"react-error-boundary@npm:^4.0.10": + version: 4.0.10 + resolution: "react-error-boundary@npm:4.0.10" + dependencies: + "@babel/runtime": ^7.12.5 + peerDependencies: + react: ">=16.13.1" + checksum: 4ad4864d2a5fc2264a24d03e83176e6a70d7adbe3c1edbdc5b0bd452a695104bc59456e23b5aea1b9729220672fe46614221daa8b3bd59327968d4aa7eb8bc71 + languageName: node + linkType: hard + +"react-hook-form@npm:^7.44.3": + version: 7.44.3 + resolution: "react-hook-form@npm:7.44.3" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + checksum: 8dc97e705d28d842c60fd79b0f07b8b045c458c234e3bf422baab87a844d0ebecbdf3e349538eaaa5c12366cd85169f60a52ed84dd31cdd99729899e424548bf + languageName: node + linkType: hard + "react-hot-toast@npm:^1.0.1": version: 1.0.2 resolution: "react-hot-toast@npm:1.0.2" @@ -9932,6 +10058,16 @@ __metadata: languageName: node linkType: hard +"react-resizable-panels@npm:^0.0.45": + version: 0.0.45 + resolution: "react-resizable-panels@npm:0.0.45" + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 + checksum: fab2ae429678c3653b469814d1eafffa70568494b9fd7e90ff8e75d938d754d0ed52df408a2e55f6489bb15dedf02a51dd60b1d8bd5ad93592102844126f27a7 + languageName: node + linkType: hard + "react-router@npm:5.3.1": version: 5.3.1 resolution: "react-router@npm:5.3.1" @@ -11633,23 +11769,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^4.6.2": - version: 4.6.2 - resolution: "typescript@npm:4.6.2" +"typescript@npm:5.1.5": + version: 5.1.5 + resolution: "typescript@npm:5.1.5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 8a44ed7e6f6c4cb1ebe8cf236ecda2fb119d84dcf0fbd77e707b2dfea1bbcfc4e366493a143513ce7f57203c75da9d4e20af6fe46de89749366351046be7577c + checksum: 0eef8699e05ae767096924dbed633c340b4d36e953bb8ed87fb12e9dd9dcea5055ceac7182c614a556dbd346a8a82df799d330e1e286ae66e17c84e1710f6a6f languageName: node linkType: hard -"typescript@patch:typescript@^4.6.2#~builtin<compat/typescript>": - version: 4.6.2 - resolution: "typescript@patch:typescript@npm%3A4.6.2#~builtin<compat/typescript>::version=4.6.2&hash=493e53" +"typescript@patch:typescript@5.1.5#~builtin<compat/typescript>": + version: 5.1.5 + resolution: "typescript@patch:typescript@npm%3A5.1.5#~builtin<compat/typescript>::version=5.1.5&hash=493e53" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: efb83260a22ee49d4c8bdc59b3cefe54fdf51d6f563f5c3a35aa3d5e46fb12f3f1d33a36d6f9f64171e567ead1847e99cb612d0a9a74e7d44e16cad9d0bbc937 + checksum: 33020c886b1aa2e948b557aad4986cf6448b30c58915b12cac873bd35dc2260d93f71af8a661d2c9f352b5d099d9df13a59688e222e79276099b9c5d86d847be languageName: node linkType: hard @@ -12363,8 +12499,8 @@ __metadata: resolution: "zui@workspace:." dependencies: "@babel/core": ^7.17.9 - "@brimdata/zed-js": 0.0.16 - "@brimdata/zed-node": 0.0.16 + "@brimdata/zed-js": 0.0.17 + "@brimdata/zed-node": 0.0.17 "@codemirror/autocomplete": ^0.19.15 "@codemirror/closebrackets": ^0.19.1 "@codemirror/commands": ^0.19.8 @@ -12388,7 +12524,7 @@ __metadata: "@testing-library/user-event": ^14.1.1 "@types/animejs": ^3.1.2 "@types/classnames": ^2.2.10 - "@types/d3": ^5.7.2 + "@types/d3": ^6.7.0 "@types/electron-devtools-installer": ^2.2.0 "@types/fs-extra": ^9.0.1 "@types/lodash": ^4.14.161 @@ -12404,8 +12540,8 @@ __metadata: "@types/sprintf-js": ^1.1.2 "@types/styled-components": ^5.1.3 "@types/tmp": ^0.2.0 - "@typescript-eslint/eslint-plugin": ^5.16.0 - "@typescript-eslint/parser": ^5.16.0 + "@typescript-eslint/eslint-plugin": 5.60.1 + "@typescript-eslint/parser": 5.60.1 abort-controller: ^3.0.0 acorn: ^7.4.1 ajv: ^6.9.1 @@ -12417,7 +12553,7 @@ __metadata: classnames: ^2.2.6 commander: ^2.20.3 cross-fetch: ^3.1.6 - d3: ^5.16.0 + d3: ^6.7.0 date-fns: ^2.16.1 decompress: ^4.2.1 electron: ^22.0.0 @@ -12469,11 +12605,14 @@ __metadata: react-dnd: ^14.0.5 react-dnd-html5-backend: ^14.0.2 react-dom: ^18.0.0 + react-error-boundary: ^4.0.10 + react-hook-form: ^7.44.3 react-hot-toast: ^1.0.1 react-input-autosize: ^3.0.0 react-is: ^17.0.2 react-markdown: ^6.0.2 react-redux: ^8.0.5 + react-resizable-panels: ^0.0.45 react-router: 5.3.1 react-spring: ^8.0.27 react-tooltip: ^4.2.7 @@ -12489,7 +12628,7 @@ __metadata: styled-components: ^5.3.5 tmp: ^0.1.0 tree-model: ^1.0.7 - typescript: ^4.6.2 + typescript: 5.1.5 use-resize-observer: ^8.0.0 web-file-polyfill: ^1.0.4 web-streams-polyfill: ^3.2.0