diff --git a/apps/zui/jest.config.js b/apps/zui/jest.config.js index 8590394ae8..53c10dfbd5 100644 --- a/apps/zui/jest.config.js +++ b/apps/zui/jest.config.js @@ -13,6 +13,7 @@ const esModules = [ "immer", "redux", "lodash-es", + "when-clause", ].join("|") // https://github.com/gravitational/teleport/issues/33810 diff --git a/apps/zui/package.json b/apps/zui/package.json index 7fa633b047..60ae9bc600 100644 --- a/apps/zui/package.json +++ b/apps/zui/package.json @@ -75,9 +75,8 @@ "ajv": "^6.9.1", "animejs": "^3.2.0", "brimcap": "brimdata/brimcap#v1.18.0", - "bullet": "^0.0.2", + "bullet": "^0.0.7", "chalk": "^4.1.0", - "chevrotain": "^10.5.0", "chrono-node": "^2.5.0", "classnames": "^2.2.6", "commander": "^2.20.3", @@ -159,6 +158,7 @@ "utopia-core-scss": "^1.0.1", "web-file-polyfill": "^1.0.4", "web-streams-polyfill": "^3.2.0", + "when-clause": "^0.0.4", "zed": "brimdata/zed#65b575c5ff9a95c7c2bf7dd9ceb935b1a51d7676", "zui-test-data": "workspace:*" }, diff --git a/apps/zui/src/app/commands/copy-query-to-clipboard.ts b/apps/zui/src/app/commands/copy-query-to-clipboard.ts index bad8e007c2..05c4f283c7 100644 --- a/apps/zui/src/app/commands/copy-query-to-clipboard.ts +++ b/apps/zui/src/app/commands/copy-query-to-clipboard.ts @@ -1,13 +1,15 @@ import {copyToClipboard} from "src/js/lib/doc" import Queries from "src/js/state/Queries" import {createCommand} from "./command" +import {Snapshot} from "src/models/snapshot" export const copyQueryToClipboard = createCommand( "copyQueryToClipboard", ({api, getState}, id: string) => { - const q = Queries.build(getState(), id) - if (q) { - copyToClipboard(q.toString()) + const query = Queries.find(getState().queries, id) + if (query) { + const text = new Snapshot(query).queryText + copyToClipboard(text) api.toast("Copied") } } diff --git a/apps/zui/src/app/lakes/root.tsx b/apps/zui/src/app/lakes/root.tsx index e094389416..d75c7a570f 100644 --- a/apps/zui/src/app/lakes/root.tsx +++ b/apps/zui/src/app/lakes/root.tsx @@ -10,6 +10,7 @@ import LakeStatuses from "src/js/state/LakeStatuses" import styled from "styled-components" import {invoke} from "src/core/invoke" import {Active} from "src/models/active" +import {BrowserTab} from "src/models/browser-tab" const SpinnerWrap = styled.div` width: 100%; @@ -26,7 +27,9 @@ export function InitLake({children}) { useLayoutEffect(() => { if (Active.lake) { - dispatch(updateStatus(lake.id)) + dispatch(updateStatus(lake.id)).then(() => { + BrowserTab.all.forEach((tab) => tab.updateTitle()) + }) Active.lake.sync() } }, [lake?.id, status]) diff --git a/apps/zui/src/app/menus/open-query-menu.ts b/apps/zui/src/app/menus/open-query-menu.ts index 3e18fbf30c..59aa287452 100644 --- a/apps/zui/src/app/menus/open-query-menu.ts +++ b/apps/zui/src/app/menus/open-query-menu.ts @@ -1,7 +1,7 @@ import {MenuItemConstructorOptions} from "electron" import {Item} from "src/js/state/Queries/types" import {createMenu} from "src/core/menu" -import {NamedQueries} from "src/domain/handlers" +import {QueriesRunner} from "src/runners/queries-runner" export const openQueryMenu = createMenu(({api}) => { function createMenuItems(items: Item[]) { @@ -14,7 +14,7 @@ export const openQueryMenu = createMenu(({api}) => { } else { return { label: query.name, - click: () => NamedQueries.show(query.id), + click: () => new QueriesRunner().open(query.id), } } }) diff --git a/apps/zui/src/app/menus/pool-toolbar-menu.ts b/apps/zui/src/app/menus/pool-toolbar-menu.ts index 2f16519305..2f96010e9b 100644 --- a/apps/zui/src/app/menus/pool-toolbar-menu.ts +++ b/apps/zui/src/app/menus/pool-toolbar-menu.ts @@ -1,7 +1,7 @@ import {Pool} from "../../models/pool" import {createMenu} from "src/core/menu" -import {Snapshots} from "src/domain/handlers" import {chooseFiles} from "src/domain/loads/handlers" +import {QuerySession} from "src/models/query-session" export const poolToolbarMenu = createMenu((_, pool: Pool) => { return [ @@ -19,7 +19,7 @@ export const poolToolbarMenu = createMenu((_, pool: Pool) => { label: "Query Pool", iconName: "query", click: () => { - Snapshots.createAndShow({ + QuerySession.activateOrCreate().navigate({ pins: [{type: "from", value: pool.name}], value: "", }) diff --git a/apps/zui/src/app/menus/query-context-menu.ts b/apps/zui/src/app/menus/query-context-menu.ts index 8a73141bcb..43b843f17c 100644 --- a/apps/zui/src/app/menus/query-context-menu.ts +++ b/apps/zui/src/app/menus/query-context-menu.ts @@ -4,7 +4,7 @@ import {copyQueryToClipboard} from "../commands/copy-query-to-clipboard" import {deleteQueries} from "../commands/delete-queries" import {exportQueryGroup} from "../commands/export-query-group" import {createMenu} from "src/core/menu" -import {NamedQueries} from "src/domain/handlers" +import {QueriesRunner} from "src/runners/queries-runner" export const queryContextMenu = createMenu( (_, tree: TreeApi, node: NodeApi) => { @@ -32,7 +32,7 @@ export const queryContextMenu = createMenu( { label: "Open Query", visible: node.isLeaf, - click: () => NamedQueries.show(node.id), + click: () => new QueriesRunner().open(node.id), }, {type: "separator"}, { diff --git a/apps/zui/src/app/router/routes.ts b/apps/zui/src/app/router/routes.ts index 5d38d39a5b..d657ecace1 100644 --- a/apps/zui/src/app/router/routes.ts +++ b/apps/zui/src/app/router/routes.ts @@ -10,54 +10,41 @@ import {IconName} from "../../components/icon" export const root: Route = { name: "root", path: "/", - title: "Zui", } export const poolShow: Route = { name: "poolShow", - title: "", path: `/pools/:poolId`, icon: "pool", } -export const query: Route = { - name: "querySession", - title: "", - path: `/queries/:queryId`, +export const snapshotShow: Route = { + name: "snapshot", + path: "/snapshots/:id", icon: "query", } -export const queryVersion: Route = { - name: "querySession", - title: "", - path: `${query.path}/versions/:version`, - icon: "query", -} export const releaseNotes: Route = { name: "releaseNotes", - title: "Release Notes", path: `/release-notes`, icon: "doc_plain", } export const welcome: Route = { name: "welcome", - title: "Welcome to Zui", path: "/welcome", icon: "zui", } type Route = { name: string - title: string path: string icon?: IconName } export const allRoutes: Route[] = [ poolShow, - query, - queryVersion, + snapshotShow, releaseNotes, welcome, root, diff --git a/apps/zui/src/app/router/utils/paths.ts b/apps/zui/src/app/router/utils/paths.ts index 0354148940..a0c78f85c0 100644 --- a/apps/zui/src/app/router/utils/paths.ts +++ b/apps/zui/src/app/router/utils/paths.ts @@ -6,6 +6,10 @@ export function queryPath(queryId: string, version: string) { return `/queries/${queryId}/versions/${version}` } +export function snapshotPath(id: string) { + return `/snapshots/${id}` +} + export function releaseNotesPath() { return "/release-notes" } diff --git a/apps/zui/src/components/button-menu.tsx b/apps/zui/src/components/button-menu.tsx index b39baed8ac..405bed6565 100644 --- a/apps/zui/src/components/button-menu.tsx +++ b/apps/zui/src/components/button-menu.tsx @@ -39,6 +39,7 @@ export function ButtonMenu(props: { const menu = useResponsiveMenu(items) const buttons = menu.items.map((item: MenuItem, i: number) => { + if (item.whenResult === false) return null return ( onMouseDown?: MouseEventHandler buildMenu?: () => MenuItem[] @@ -73,6 +75,7 @@ export const IconButton = forwardRef(function IconButton( diff --git a/apps/zui/src/core/menu/use-menu-extension.ts b/apps/zui/src/core/menu/use-menu-extension.ts index e9b9e68cf3..2c87c7da62 100644 --- a/apps/zui/src/core/menu/use-menu-extension.ts +++ b/apps/zui/src/core/menu/use-menu-extension.ts @@ -1,8 +1,8 @@ import {useEffect, useLayoutEffect, useState} from "react" import {MenuItem} from "src/core/menu" -import {compile} from "../when/compile" import {invoke} from "../invoke" import {useTabId} from "src/util/hooks/use-tab-id" +import {evaluate} from "when-clause" export function useMenuExtension( name: string, @@ -13,23 +13,21 @@ export function useMenuExtension( const tabId = useTabId() useLayoutEffect(() => { - invoke("menus.extend", name, menuItems) - .then((extended) => compileMenuItems(extended, whenContext)) - .then((compiled) => setItems(compiled)) - }, [name, menuItems, whenContext, tabId]) + invoke("menus.extend", name, menuItems).then((items) => setItems(items)) + }, [name, tabId]) useEffect(() => { return global.zui.on("menus.update", (e, menu, id, update) => { if (menu !== name) return - setItems( + setItems((items) => items.map((item: MenuItem) => { return item.id === id ? {...item, ...update} : item }) ) }) - }, [items, name]) + }, [name]) - return items + return compileMenuItems(items, whenContext) } function compileMenuItems(items: MenuItem[], context: Record) { @@ -37,7 +35,7 @@ function compileMenuItems(items: MenuItem[], context: Record) { .map((item) => { return { ...item, - whenResult: compile(item.when, context), + whenResult: item.when && evaluate(item.when, context), priority: item.priority ?? 0, } }) diff --git a/apps/zui/src/core/query/run.ts b/apps/zui/src/core/query/run.ts index f04359f5a7..3ba74d9abe 100644 --- a/apps/zui/src/core/query/run.ts +++ b/apps/zui/src/core/query/run.ts @@ -37,26 +37,28 @@ const run = createHandler( const prevVals = select(Results.getValues(id)) const prevShapes = select(Results.getShapes(id)) const paginatedQuery = select(Results.getPaginatedQuery(id)) - const {signal} = asyncTasks.createOrReplace([tabId, id]) - try { - const res = await query(paginatedQuery, {signal}) - await 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})) - }) - dispatch(Results.success({id, tabId, count: res.rows.length})) - return res - } catch (e) { - if (isAbortError(e)) { + const task = await asyncTasks.createOrReplace([tabId, id]) + task.run(async (signal) => { + try { + const res = await query(paginatedQuery, {signal}) + await 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})) + }) + 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 - } else { - dispatch( - Results.error({id, tabId, error: ErrorFactory.create(e).message}) - ) } - return null - } + }) } ) diff --git a/apps/zui/src/core/view-handler.ts b/apps/zui/src/core/view-handler.ts index dfc5285ee6..5d8d52d6f5 100644 --- a/apps/zui/src/core/view-handler.ts +++ b/apps/zui/src/core/view-handler.ts @@ -2,6 +2,7 @@ import {Dispatch, State, Store} from "src/js/state/types" import {ipc} from "src/modules/bullet/view" import {invoke} from "./invoke" import toast from "react-hot-toast" +import {useEffect} from "react" type Selector = (state: State, ...args: any) => any @@ -26,4 +27,19 @@ export class ViewHandler { protected request(path: string, params?: object) { return ipc.request(path, params) } + + protected listen(eventMap: Record) { + useEffect(() => { + const offs = [] + for (const [event, handler] of Object.entries(eventMap)) { + offs.push( + global.zui?.on(event, (_event, ...args: any[]) => handler(...args)) + ) + } + + return () => { + offs.forEach((off) => off()) + } + }) + } } diff --git a/apps/zui/src/core/when/ast.ts b/apps/zui/src/core/when/ast.ts deleted file mode 100644 index 4809e8bdc4..0000000000 --- a/apps/zui/src/core/when/ast.ts +++ /dev/null @@ -1,93 +0,0 @@ -export interface AstNode { - resolve(scope: Scope): Value -} - -export class Scope { - storage = new Map() - - constructor(parent: Scope | null, initialValues = {}) { - for (let [key, value] of Object.entries(initialValues)) { - this.set(new Symbol(key), new Value(value)) - } - } - - set(sym: Symbol, value: Value) { - this.storage.set(sym.name, value) - } - - get(sym: Symbol) { - if (!this.storage.has(sym.name)) { - throw new Error(`Could not find symbol '${sym.name}'`) - } - return this.storage.get(sym.name) - } - - has(sym: Symbol) { - return this.storage.has(sym.name) - } -} - -export class Value { - constructor(public val: any) {} - - resolve(_scope: Scope) { - return this - } - jsEquals(jsVal) { - return this.val === jsVal - } -} - -export class Symbol { - constructor(public name: string) {} - - resolve(scope: Scope) { - return scope.get(this) - } -} - -type BinOpName = - | "add" - | "sub" - | "mul" - | "div" - | "eq" - | "neq" - | "gt" - | "gte" - | "lt" - | "lte" - -export class BinOp { - constructor(public op: BinOpName, public a: AstNode, public b: AstNode) {} - - resolveLeft(scope: Scope) { - return this.a.resolve(scope) - } - - resolveRight(scope: Scope) { - if (this.b instanceof Symbol) { - if (scope.has(this.b)) return scope.get(this.b) - else return new Value(this.b.name) - } else { - return this.b.resolve(scope) - } - } - - resolve(scope: Scope): Value { - const a = this.resolveLeft(scope).val - const b = this.resolveRight(scope).val - - if (this.op === "add") return new Value(a + b) - if (this.op === "sub") return new Value(a - b) - if (this.op === "mul") return new Value(a * b) - if (this.op === "div") return new Value(a / b) - if (this.op === "eq") return new Value(a == b) - if (this.op === "neq") return new Value(a != b) - if (this.op === "gte") return new Value(a >= b) - if (this.op === "lte") return new Value(a <= b) - if (this.op === "gt") return new Value(a > b) - if (this.op === "lt") return new Value(a < b) - throw new Error("Missing: " + this.op) - } -} diff --git a/apps/zui/src/core/when/compile.ts b/apps/zui/src/core/when/compile.ts deleted file mode 100644 index c7bcf16def..0000000000 --- a/apps/zui/src/core/when/compile.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {AstNode, Scope} from "./ast" -import {Grammar, Semantics} from "./parser" - -export function compile( - input: string | undefined = undefined, - context: Record -): boolean { - if (!input) return true - const match = Grammar.match(input) - if (match.failed()) { - throw new Error( - "Syntax error in when clause '" + input + "' " + match.message - ) - } - const ast: AstNode = Semantics(match).ast() - const scope = new Scope(null, context) - const result = ast.resolve(scope).val - if (typeof result !== "boolean") { - throw new Error("When clause must return a boolean") - } - return result -} diff --git a/apps/zui/src/core/when/parser.test.ts b/apps/zui/src/core/when/parser.test.ts deleted file mode 100644 index 9a71790698..0000000000 --- a/apps/zui/src/core/when/parser.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {AstNode, Scope} from "./ast" -import {Grammar, Semantics} from "./parser" - -const GLOBAL = new Scope(null, { - x: 42, - "results.view": "INSPECTOR", - "results.rowCount": 500, -}) - -function assert(input, answer) { - const match = Grammar.match(input) - if (match.failed()) - throw new Error("input failed to match " + input + match.message) - - const ast: AstNode = Semantics(match).ast() - const result = ast.resolve(GLOBAL) - expect(result.val).toEqual(answer) -} - -test("expressions", () => { - assert("1", 1) - assert("123", 123) - assert("(123)", 123) - assert("(((99)))", 99) - assert("1 + 1", 2) - assert("1 - 1", 0) - assert("3 * 2", 6) - assert("(10 + 6) / 4", 4) - assert("5==5", true) - assert("5==1", false) - assert("5 != 3", true) - assert("5 != 5", false) - assert("5 >= 4", true) - assert("5 >= 5", true) - assert("5 >= 6", false) - assert("5 <= 6", true) - assert("5 <= 5", true) - assert("5 <= 4", false) - assert("5 > 1", true) - assert("5 > 10", false) - assert("5 < 10", true) - assert("5 < 1", false) - assert("x == 42", true) - assert("x != 42", false) - assert("results.view == INSPECTOR", true) - assert("results.rowCount < 30", false) -}) diff --git a/apps/zui/src/core/when/parser.ts b/apps/zui/src/core/when/parser.ts deleted file mode 100644 index 86177598e1..0000000000 --- a/apps/zui/src/core/when/parser.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {grammar} from "ohm-js" -import {BinOp, Symbol, Value} from "./ast" - -export const Grammar = grammar(String.raw` - When { - Expr = MathOp | Term - MathOp = Mul | Div | Add | Sub | Eq | Neq | Lt | Lte | Gt | Gte - Add = Expr "+" Term - Sub = Expr "-" Term - Mul = Expr "*" Term - Div = Expr "/" Term - Eq = Expr "==" Term - Neq = Expr "!=" Term - Gt = Expr ">" Term - Gte = Expr ">=" Term - Lt = Expr "<" Term - Lte = Expr "<=" Term - Group = "(" Expr ")" - - Term = Group | identifier | Number - identifier = letter (letter|digit|".")* - Number = digit+ - } -`) - -export const Semantics = Grammar.createSemantics() - -Semantics.addOperation("ast", { - Add: (a, _, b) => new BinOp("add", a.ast(), b.ast()), - Sub: (a, _, b) => new BinOp("sub", a.ast(), b.ast()), - Mul: (a, _, b) => new BinOp("mul", a.ast(), b.ast()), - Div: (a, _, b) => new BinOp("div", a.ast(), b.ast()), - Eq: (a, _, b) => new BinOp("eq", a.ast(), b.ast()), - Neq: (a, _, b) => new BinOp("neq", a.ast(), b.ast()), - Gt: (a, _, b) => new BinOp("gt", a.ast(), b.ast()), - Gte: (a, _, b) => new BinOp("gte", a.ast(), b.ast()), - Lt: (a, _, b) => new BinOp("lt", a.ast(), b.ast()), - Lte: (a, _, b) => new BinOp("lte", a.ast(), b.ast()), - Number: (n) => new Value(parseInt(n.sourceString)), - identifier: function (_a, _b) { - return new Symbol(this.sourceString) - }, - Group: (_1, a, _2) => a.ast(), -}) diff --git a/apps/zui/src/css/_utilities.scss b/apps/zui/src/css/_utilities.scss index 60018cc012..d0ab72cc37 100644 --- a/apps/zui/src/css/_utilities.scss +++ b/apps/zui/src/css/_utilities.scss @@ -151,7 +151,8 @@ } .color\:fg-less, -.text-less { +.text-less, +.text-meta { color: var(--fg-color-less); } @@ -171,6 +172,10 @@ font-weight: bold; } +.weight\:medium { + font-weight: 500; +} + .text-center { text-align: center; } diff --git a/apps/zui/src/css/blocks/_tab-bar.scss b/apps/zui/src/css/blocks/_tab-bar.scss index 73fd7bb713..e3f35f89be 100644 --- a/apps/zui/src/css/blocks/_tab-bar.scss +++ b/apps/zui/src/css/blocks/_tab-bar.scss @@ -41,7 +41,8 @@ position: relative; /* Sizes */ - flex: 0 1 max-content; + inline-size: 180px; + flex: 0 1 180px; block-size: 100%; min-inline-size: 0; padding-inline-start: var(--gutter); diff --git a/apps/zui/src/css/compositions/_gutter.scss b/apps/zui/src/css/compositions/_gutter.scss index 807a716db9..dadee43ca8 100644 --- a/apps/zui/src/css/compositions/_gutter.scss +++ b/apps/zui/src/css/compositions/_gutter.scss @@ -17,3 +17,7 @@ .gutter-inline-start { padding-inline-start: var(--gutter); } + +.gutter-half { + padding-inline: var(--half-gutter); +} diff --git a/apps/zui/src/domain/handlers.ts b/apps/zui/src/domain/handlers.ts index c223e61f9a..5d1a46bbf9 100644 --- a/apps/zui/src/domain/handlers.ts +++ b/apps/zui/src/domain/handlers.ts @@ -7,5 +7,3 @@ import "./window/handlers" import "./session/handlers/navigation" import "./loads/handlers" import "./pools/handlers" -export * as NamedQueries from "./named-queries/handlers" -export * as Snapshots from "./snapshots/handlers" diff --git a/apps/zui/src/domain/messages.ts b/apps/zui/src/domain/messages.ts index 10d3069ba3..43822e0618 100644 --- a/apps/zui/src/domain/messages.ts +++ b/apps/zui/src/domain/messages.ts @@ -11,7 +11,6 @@ import {UpdatesOperations} from "./updates/messages" import {LoadsHandlers, LoadsOperations} from "./loads/messages" import {CommandsOperations} from "./commands/messages" import {EditorHandlers, EditorOperations} from "./editor/messages" -import {NamedQueriesHandlers} from "./named-queries/messages" export type Handlers = ResultsHandlers & MenusHandlers & @@ -20,8 +19,7 @@ export type Handlers = ResultsHandlers & SessionHandlers & LoadsHandlers & PoolsHandlers & - EditorHandlers & - NamedQueriesHandlers + EditorHandlers export type Operations = PoolsOperations & LegacyOperations & diff --git a/apps/zui/src/domain/named-queries/handlers.ts b/apps/zui/src/domain/named-queries/handlers.ts deleted file mode 100644 index f35d463bb3..0000000000 --- a/apps/zui/src/domain/named-queries/handlers.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {createHandler} from "src/core/handlers" -import {Active} from "src/models/active" -import {EditorSnapshot} from "src/models/editor-snapshot" -import {NamedQuery} from "src/models/named-query" -import {Session} from "src/models/session" - -/** - * This handler is called when the user submits the form to name their - * query for the first time. - */ -export const create = createHandler(async ({oldApi}, name: string) => { - const {parentId: _, ...attrs} = Active.snapshot.attrs - const query = await oldApi.queries.create({name, versions: [attrs]}) - const namedQuery = new NamedQuery({ - id: query.id, - name: query.name, - }) - Active.session.navigate(namedQuery.lastSnapshot, namedQuery) -}) - -/** - * This handler is called when the user updates a query to a new version. - */ -export const update = createHandler("namedQueries.update", () => { - const {session, snapshot} = Active - const {namedQuery} = session - const newSnapshot = snapshot.clone({parentId: namedQuery.id}) - newSnapshot.save() - session.navigate(newSnapshot, namedQuery) -}) - -/* This handler is called when you want to display a named query in a session */ -export const show = createHandler((_, id: string, snapshotId?: string) => { - const query = NamedQuery.find(id) - const snapshot = snapshotId - ? EditorSnapshot.find(query.id, snapshotId) - : query.lastSnapshot - - Session.activateLastFocused() - Active.session.navigate(snapshot, query) -}) diff --git a/apps/zui/src/domain/named-queries/messages.ts b/apps/zui/src/domain/named-queries/messages.ts deleted file mode 100644 index 907768cbcd..0000000000 --- a/apps/zui/src/domain/named-queries/messages.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as handlers from "./handlers" - -export type NamedQueriesHandlers = { - "namedQueries.update": typeof handlers.update -} diff --git a/apps/zui/src/domain/session/handlers/pins.ts b/apps/zui/src/domain/session/handlers/pins.ts index 1e52928cc1..a974c11146 100644 --- a/apps/zui/src/domain/session/handlers/pins.ts +++ b/apps/zui/src/domain/session/handlers/pins.ts @@ -9,8 +9,8 @@ import {submitSearch} from "src/domain/session/handlers" import {createHandler} from "src/core/handlers" import ZuiApi from "src/js/api/zui-api" import Selection from "src/js/state/Selection" -import {Session} from "src/models/session" import {Active} from "src/models/active" +import {QuerySession} from "src/models/query-session" export const createPinFromEditor = createHandler( "session.createPinFromEditor", @@ -44,9 +44,9 @@ export const createFromPin = createHandler( export const setFromPin = createHandler( "session.setFromPin", ({dispatch}, value: string) => { - Session.activateLastFocused() + const session = QuerySession.activateOrCreate() dispatch(Editor.setFrom(value)) - Active.session.navigate(Active.snapshot, Active.session.namedQuery) + session.navigate(Active.editorState) } ) diff --git a/apps/zui/src/domain/session/handlers/queries.ts b/apps/zui/src/domain/session/handlers/queries.ts index a910630e6c..7c065639a4 100644 --- a/apps/zui/src/domain/session/handlers/queries.ts +++ b/apps/zui/src/domain/session/handlers/queries.ts @@ -1,10 +1,6 @@ import {createHandler} from "src/core/handlers" -import Current from "src/js/state/Current" import Layout from "src/js/state/Layout" -import {plusOne} from "src/util/plus-one" import {submitSearch} from "./submit-search" -import {Active} from "src/models/active" -import {create} from "src/domain/named-queries/handlers" export const editQuery = createHandler("session.editQuery", ({dispatch}) => { dispatch(Layout.showTitleForm()) @@ -14,23 +10,6 @@ export const runQuery = createHandler("session.runQuery", () => { submitSearch() }) -export const saveAsNewQuery = createHandler( - "session.saveAsNewQuery", - async ({select, dispatch}) => { - const name = select(Current.getActiveQuery).name() - const newName = plusOne(name) - await create(newName) - setTimeout(() => { - dispatch(Layout.showTitleForm()) - }) - } -) - -export const resetQuery = createHandler("session.resetQuery", () => { - const {session} = Active - session.navigate(session.snapshot) -}) - export const fetchQueryInfo = createHandler( ({invoke}, query: string, pool?: string) => invoke("editor.describe", query, pool) diff --git a/apps/zui/src/domain/session/handlers/submit-search.ts b/apps/zui/src/domain/session/handlers/submit-search.ts index 573a8d2fcf..bc14b559b0 100644 --- a/apps/zui/src/domain/session/handlers/submit-search.ts +++ b/apps/zui/src/domain/session/handlers/submit-search.ts @@ -1,24 +1,14 @@ import {createHandler} from "src/core/handlers" import {Active} from "src/models/active" -/** - * Save the active snapshot under the session id. - * - * Redirect the app to the previous parent id, - * but the active snapshot id that was just saved. - * - * The session page is essentially a form to create - * a new editor snapshot under that session id. - * - * It's should be thought of as POST /session/:id/snapshots - */ -export const submitSearch = createHandler(async () => { - const {session} = Active - const nextSnapshot = Active.snapshot +export const submitSearch = createHandler(() => { + const session = Active.querySession + const snapshot = Active.snapshot + const editorState = Active.editorState - if (nextSnapshot.equals(session.snapshot)) { - session.load() + if (snapshot.equals(editorState)) { + session.reload() } else { - session.navigate(Active.snapshot, session.namedQuery) + session.navigate(editorState) } }) diff --git a/apps/zui/src/domain/session/messages.ts b/apps/zui/src/domain/session/messages.ts index 38004dd41a..57017f1cb1 100644 --- a/apps/zui/src/domain/session/messages.ts +++ b/apps/zui/src/domain/session/messages.ts @@ -8,8 +8,6 @@ export type SessionHandlers = { "session.createPinFromEditor": typeof handlers.createPinFromEditor "session.editQuery": typeof handlers.editQuery "session.runQuery": typeof handlers.runQuery - "session.saveAsNewQuery": typeof handlers.saveAsNewQuery - "session.resetQuery": typeof handlers.resetQuery "session.toggleHistoryPane": typeof handlers.toggleHistoryPane "session.createFromPin": typeof handlers.createFromPin "session.createPin": typeof handlers.createPin diff --git a/apps/zui/src/domain/snapshots/handlers.ts b/apps/zui/src/domain/snapshots/handlers.ts deleted file mode 100644 index d89887fbb4..0000000000 --- a/apps/zui/src/domain/snapshots/handlers.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {createHandler} from "src/core/handlers" -import {Active} from "src/models/active" -import {EditorSnapshot} from "src/models/editor-snapshot" -import {NamedQuery} from "src/models/named-query" -import {Session} from "src/models/session" - -/** - * This is called when you click on a session history entry. - */ - -type Args = { - sessionId: string - namedQueryId?: string - snapshotId: string -} - -export const show = createHandler((ctx, args: Args) => { - const {session} = Active - const namedQuery = NamedQuery.find(args.namedQueryId) - const snapshot = EditorSnapshot.find(session.id, args.snapshotId) - Active.session.navigate(snapshot, namedQuery) -}) - -export const createAndShow = createHandler( - (ctx, args: Partial) => { - Session.activateLastFocused() - Active.session.navigate(new EditorSnapshot(args)) - } -) diff --git a/apps/zui/src/domain/window/handlers.ts b/apps/zui/src/domain/window/handlers.ts index cdb589c850..c9781cd004 100644 --- a/apps/zui/src/domain/window/handlers.ts +++ b/apps/zui/src/domain/window/handlers.ts @@ -2,12 +2,10 @@ import {createHandler} from "src/core/handlers" import toast from "react-hot-toast" import Tabs from "src/js/state/Tabs" import {welcomePath} from "src/app/router/utils/paths" -import {QueryParams} from "src/js/api/queries/types" import {PaneName} from "src/js/state/Layout/types" import Appearance from "src/js/state/Appearance" import Layout from "src/js/state/Layout" import Modal from "src/js/state/Modal" -import {Snapshots} from "../handlers" export const showErrorMessage = createHandler( "window.showErrorMessage", @@ -37,13 +35,6 @@ export const showWelcomePage = createHandler( } ) -export const query = createHandler( - "window.query", - (ctx, params: QueryParams) => { - Snapshots.createAndShow(params) - } -) - export const openTab = createHandler( "window.openTab", ({dispatch}, path: string) => { diff --git a/apps/zui/src/domain/window/messages.ts b/apps/zui/src/domain/window/messages.ts index 4b27be060c..1498f9f665 100644 --- a/apps/zui/src/domain/window/messages.ts +++ b/apps/zui/src/domain/window/messages.ts @@ -6,7 +6,6 @@ export type WindowHandlers = { "window.showMessage": typeof handlers.showMessage "window.showSuccessMessage": typeof handlers.showSuccessMessage "window.showWelcomePage": typeof handlers.showWelcomePage - "window.query": typeof handlers.query "window.openTab": typeof handlers.openTab } diff --git a/apps/zui/src/domain/window/plugin-api.ts b/apps/zui/src/domain/window/plugin-api.ts index fca89279c4..0b988aece1 100644 --- a/apps/zui/src/domain/window/plugin-api.ts +++ b/apps/zui/src/domain/window/plugin-api.ts @@ -1,5 +1,4 @@ import {sendToWindow} from "src/core/ipc" -import {QueryParams} from "src/js/api/queries/types" export class WindowApi { id: string | null = null @@ -17,10 +16,6 @@ export class WindowApi { sendToWindow(this.id, "window.showSuccessMessage", message) } - query(params: QueryParams) { - sendToWindow(this.id, "window.query", params) - } - sync(args: {id: string; lakeId: string}) { this.id = args.id this.lakeId = args.lakeId diff --git a/apps/zui/src/electron/ops/export-query-group-op.ts b/apps/zui/src/electron/ops/export-query-group-op.ts index 721ad302aa..64655762e1 100644 --- a/apps/zui/src/electron/ops/export-query-group-op.ts +++ b/apps/zui/src/electron/ops/export-query-group-op.ts @@ -1,6 +1,5 @@ import Queries from "src/js/state/Queries" import {serializeQueryLib} from "src/js/state/Queries/parsers" -import QueryVersions from "src/js/state/QueryVersions" import {createOperation} from "../../core/operations" import fs from "fs-extra" @@ -9,8 +8,7 @@ export const exportQueryGroupOp = createOperation( ({main}, groupId: string, filePath: string) => { const state = main.store.getState() const group = Queries.getGroupById(groupId)(state) - const versions = QueryVersions.raw(state) - const json = serializeQueryLib(group, versions) + const json = serializeQueryLib(group) return fs.writeJSON(filePath, json) } ) diff --git a/apps/zui/src/electron/ops/import-queries-op.ts b/apps/zui/src/electron/ops/import-queries-op.ts index 6236b0e744..bc5767feeb 100644 --- a/apps/zui/src/electron/ops/import-queries-op.ts +++ b/apps/zui/src/electron/ops/import-queries-op.ts @@ -1,7 +1,6 @@ import Queries from "src/js/state/Queries" import {isGroup, isQuery} from "src/js/state/Queries/helpers" -import {parseJSONLib} from "src/js/state/Queries/parsers" -import QueryVersions from "src/js/state/QueryVersions" +import {flattenItemTree, parseJSONLib} from "src/js/state/Queries/parsers" import {createOperation} from "../../core/operations" export const importQueriesOp = createOperation( @@ -13,19 +12,16 @@ export const importQueriesOp = createOperation( } catch { return {error: "File is not JSON"} } - const {libRoot, versions} = json + const {libRoot} = json if (!isValidQueryGroup(libRoot)) { return {error: "Incorrect query format"} } + const flatTree = flattenItemTree(libRoot) + const size = flatTree.filter((item) => !!item.value).length main.store.dispatch(Queries.addItem(libRoot, "root")) - for (let queryId in versions) { - const version = versions[queryId] - main.store.dispatch(QueryVersions.at(queryId).sync([version])) - } - - return {size: Object.keys(versions).length, id: libRoot.id} + return {size, id: libRoot.id} } ) diff --git a/apps/zui/src/electron/session.ts b/apps/zui/src/electron/session.ts index 4f9527f50f..49310809a5 100644 --- a/apps/zui/src/electron/session.ts +++ b/apps/zui/src/electron/session.ts @@ -32,7 +32,6 @@ export default function session(path: string | null) { const f = file(path) version = migrator.getLatestVersion() - if (await f.exists()) { return await f .read() @@ -79,8 +78,10 @@ async function migrate(appState, migrator): Promise { log.info("migrations started") const nextState = migrator.runPending(state) log.info(`migrated to version: ${nextState.version}`) + console.log(nextState) return nextState } catch (e) { + console.log(e) log.error("unable to migrate") log.error(e) return freshState(migrator.getLatestVersion()) diff --git a/apps/zui/src/js/api/current/current-api.ts b/apps/zui/src/js/api/current/current-api.ts index f7610adccf..7fe81d17bf 100644 --- a/apps/zui/src/js/api/current/current-api.ts +++ b/apps/zui/src/js/api/current/current-api.ts @@ -28,8 +28,4 @@ export class CurrentApi { get value() { return LogDetails.build(this.getState()) } - - get query() { - return Current.getActiveQuery(this.getState()) - } } diff --git a/apps/zui/src/js/api/queries/export.ts b/apps/zui/src/js/api/queries/export.ts index c5730d2298..38b66a29c4 100644 --- a/apps/zui/src/js/api/queries/export.ts +++ b/apps/zui/src/js/api/queries/export.ts @@ -1,9 +1,7 @@ import Queries from "src/js/state/Queries" import {serializeQueryLib} from "src/js/state/Queries/parsers" -import QueryVersions from "src/js/state/QueryVersions" export const queriesExport = (groupId: string) => (dispatch, getState) => { const group = Queries.getGroupById(groupId)(getState()) - const versions = QueryVersions.raw(getState()) - return serializeQueryLib(group, versions) + return serializeQueryLib(group) } diff --git a/apps/zui/src/js/api/queries/queries-api.ts b/apps/zui/src/js/api/queries/queries-api.ts index 3718979d6f..bcfe727a02 100644 --- a/apps/zui/src/js/api/queries/queries-api.ts +++ b/apps/zui/src/js/api/queries/queries-api.ts @@ -1,12 +1,7 @@ import {nanoid} from "@reduxjs/toolkit" import Queries from "src/js/state/Queries" -import QueryVersions from "src/js/state/QueryVersions" -import {QueryVersion} from "src/js/state/QueryVersions/types" import {AppDispatch, GetState} from "../../state/types" import {queriesImport} from "./import" -import {CreateQueryParams, QueryParams} from "./types" -import {Query} from "src/js/state/Queries/types" -import SessionQueries from "src/js/state/SessionQueries" import {invoke} from "src/core/invoke" export class QueriesApi { @@ -24,53 +19,18 @@ export class QueriesApi { return invoke("exportQueries", groupId, filePath) } - find(id: string) { - return Queries.build(this.getState(), id) - } - - async create(params: CreateQueryParams) { - const query = {id: params.id ?? nanoid(), name: params.name ?? ""} - const versions = params.versions ?? [QueryVersions.initial()] - this.dispatch(Queries.addItem(query, params.parentId)) - versions.forEach((version) => this.createEditorSnapshot(query.id, version)) - return this.find(query.id) - } - createGroup(name: string, parentId: string) { const item = {name, id: nanoid(), items: []} this.dispatch(Queries.addItem(item, parentId)) return item } - async update(args: {id: string; changes: Partial}) { - this.dispatch(Queries.editItem(args)) - } - async delete(id: string | string[]) { const ids = Array.isArray(id) ? id : [id] await Promise.all( ids.map(async (id) => { - this.dispatch(QueryVersions.at(id).deleteAll()) this.dispatch(Queries.removeItems([id])) }) ) } - - rename(id: string, name: string) { - this.update({id, changes: {name}}) - } - - createEditorSnapshot(queryId: string, params: QueryVersion | QueryParams) { - const ts = new Date().toISOString() - const id = nanoid() - const version = {ts, version: id, ...params} - this.dispatch(QueryVersions.at(queryId).create(version)) - return version - } - - getSource(id: string) { - if (SessionQueries.find(this.getState(), id)) return "session" - if (Queries.find(this.getState(), id)) return "local" - return null - } } diff --git a/apps/zui/src/js/api/queries/types.ts b/apps/zui/src/js/api/queries/types.ts index 1f1499a4d2..656c0affe0 100644 --- a/apps/zui/src/js/api/queries/types.ts +++ b/apps/zui/src/js/api/queries/types.ts @@ -1,11 +1,9 @@ import {QueryPin} from "src/js/state/Editor/types" import {Query} from "src/js/state/Queries/types" -import {QueryVersion} from "src/js/state/QueryVersions/types" export type CreateQueryParams = Partial & { type?: QuerySource parentId?: string | null - versions?: QueryVersion[] } export type OpenQueryOptions = { diff --git a/apps/zui/src/js/initializers/init-domain-models.ts b/apps/zui/src/js/initializers/init-domain-models.ts index 106c22f512..d88ceae77c 100644 --- a/apps/zui/src/js/initializers/init-domain-models.ts +++ b/apps/zui/src/js/initializers/init-domain-models.ts @@ -1,8 +1,10 @@ import {DomainModel} from "src/core/domain-model" import {Store} from "../state/types" import {Entity} from "bullet" +import {ApplicationRunner} from "src/runners/application-runner" export function initDomainModels(args: {store: Store}) { DomainModel.store = args.store Entity.store = args.store + ApplicationRunner.store = args.store } diff --git a/apps/zui/src/js/models/query-model.ts b/apps/zui/src/js/models/query-model.ts deleted file mode 100644 index 09dc0115f1..0000000000 --- a/apps/zui/src/js/models/query-model.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {Query} from "src/js/state/Queries/types" -import {last} from "lodash" -import {QueryVersion} from "src/js/state/QueryVersions/types" -import {QuerySource} from "src/js/api/queries/types" - -export class QueryModel implements Query { - id: string - name: string - description?: string - tags?: string[] - isReadOnly?: boolean - current: QueryVersion - versions: QueryVersion[] - source: QuerySource - - constructor(raw: Query, versions: QueryVersion[], source: QuerySource) { - this.id = raw.id - this.name = raw.name - this.source = source - this.versions = versions - this.description = raw.description || "" - this.tags = raw.tags || [] - this.isReadOnly = raw.isReadOnly || false - this.current = last(versions) - } - - get value() { - return this.current?.value ?? "" - } - - get pins() { - return this.current?.pins ?? [] - } - - hasVersion(version: string): boolean { - return !!this.versions?.map((v) => v.version).includes(version) - } - - latestVersion(): QueryVersion { - return last(this.versions) ?? null - } - - latestVersionId() { - if (this.latestVersion()) { - return this.latestVersion().version - } else { - return null - } - } - - serialize(): Query { - return { - id: this.id, - name: this.name, - description: this.description, - tags: this.tags, - isReadOnly: this.isReadOnly, - } - } -} diff --git a/apps/zui/src/js/models/tab.ts b/apps/zui/src/js/models/tab.ts deleted file mode 100644 index a5fa2d505f..0000000000 --- a/apps/zui/src/js/models/tab.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {whichRoute} from "src/app/router/routes" -import get from "lodash/get" -import {PoolsState} from "../state/Pools/types" -import {LakesState} from "../state/Lakes/types" - -export default function tab( - tabId: string, - lakes: LakesState, - pools: PoolsState, - queryIdNameMap: any, - lakeId: string -) { - const history = global.tabHistories.getOrCreate(tabId) - const route = whichRoute(history.location.pathname) - return { - id: tabId, - title() { - if (route) { - return compileTitle( - route, - history.location, - lakes, - pools, - queryIdNameMap, - lakeId - ) - } else { - return "Zui" - } - }, - icon() { - return route?.icon - }, - } -} - -/** - * Replaces keywords like with the - * actual names of the current lake pool and query name. - */ -function compileTitle(route, location, lakes, pools, queryIdNameMap, lakeId) { - let title = route.title - const {queryId, poolId, version} = route.match.params - title = title.replace("", get(lakes, [lakeId, "name"], "")) - if (poolId) { - title = title.replace( - "", - get(pools, [lakeId, poolId, "data", "name"], "Not Found") - ) - } - if (queryId) { - title = title.replace("", queryIdNameMap[queryId] || "Query Page") - } - if (version) { - title = title.replace("", version) - } - return title -} - -export type TabModel = ReturnType diff --git a/apps/zui/src/js/state/Current/selectors.ts b/apps/zui/src/js/state/Current/selectors.ts index 1603db8230..396a0578cd 100644 --- a/apps/zui/src/js/state/Current/selectors.ts +++ b/apps/zui/src/js/state/Current/selectors.ts @@ -4,22 +4,15 @@ import {State} from "../types" import Lakes from "../Lakes" import {MemoryHistory} from "history" import {Pool} from "src/models/pool" -import Queries from "../Queries" -import {QueryModel} from "src/js/models/query-model" -import QueryVersions from "../QueryVersions" -import {query, queryVersion, whichRoute} from "src/app/router/routes" -import SessionHistories from "../SessionHistories" +import {snapshotShow, whichRoute} from "src/app/router/routes" import {createSelector} from "@reduxjs/toolkit" -import {QueryVersion} from "../QueryVersions/types" -import {ActiveQuery} from "src/models/active-query" -import SessionQueries from "../SessionQueries" -import memoizeOne from "memoize-one" -import {entitiesToArray} from "../utils" import {Lake} from "src/models/lake" import {defaultLake} from "src/js/initializers/initLakeParams" import {getActive} from "../Tabs/selectors" import QueryInfo from "../QueryInfo" -import {EditorSnapshot} from "src/models/editor-snapshot" +import {Snapshot} from "src/models/snapshot" +import Queries from "../Queries" +import Editor from "../Editor" export const getHistory = ( state, @@ -38,68 +31,6 @@ export const getLocation = (state: State) => { return getHistory(state)?.location } -export const getQueryUrlParams = createSelector(getLocation, (location) => { - const path = location.pathname - const routes = [queryVersion.path, query.path] - const match = matchPath<{queryId: string; version: string}>(path, routes) - return match?.params ?? {queryId: "", version: ""} -}) - -export const getVersion = (state: State): QueryVersion => { - const {queryId, version} = getQueryUrlParams(state) - const tabId = getTabId(state) - return ( - QueryVersions.at(queryId).find(state, version) || - QueryVersions.at(tabId).find(state, version) - ) -} - -export const getQueryText = createSelector(getVersion, (version) => { - return new EditorSnapshot(version).toQueryText() -}) - -const getRawSession = (state: State) => { - const id = getSessionId(state) - return SessionQueries.find(state, id) -} - -const memoGetVersions = memoizeOne(entitiesToArray) - -const getSessionVersions = (state: State) => { - const id = getSessionId(state) - const entities = QueryVersions.at(id).entities(state) - const ids = QueryVersions.at(id).ids(state) - return memoGetVersions(ids, entities) -} - -export const getNamedQuery = (state: State) => { - const queryId = getSessionRouteParentId(state) - return Queries.build(state, queryId) -} - -export const getSessionRouteParentId = (state: State) => { - const {queryId} = getQueryUrlParams(state) - return queryId -} - -export const getSession = createSelector( - getRawSession, - getSessionVersions, - (query, versions) => { - if (!query) return null - return new QueryModel(query, versions, "session") - } -) - -export const getActiveQuery = createSelector( - getSession, - getNamedQuery, - getVersion, - (session, query, version) => { - return new ActiveQuery(session, query, version || QueryVersions.initial()) - } -) - export const getPoolId = (state) => { type Params = {poolId?: string} const match = matchPath(getLocation(state).pathname, [ @@ -112,6 +43,38 @@ export const getLakeId = (state: State) => { return state.window.lakeId ?? defaultLake().id } +export const getSnapshotId = (state) => { + const {pathname} = getLocation(state) + const route = snapshotShow.path + const match = matchPath(pathname, [route]) + return match?.params?.id || null +} +export const getSnapshot = createSelector(getSnapshotId, (id) => + Snapshot.find(id) +) + +export const getQuery = createSelector( + getSnapshot, + (state) => state.queries, + (snapshot, queries) => { + return Queries.find(queries, snapshot.queryId) + } +) + +export const getQueryText = createSelector( + getSnapshot, + (snapshot) => snapshot.queryText +) + +export const getQueryIsModified = createSelector( + getQuery, + Editor.getSnapshot, + (query, editorState) => { + const snapshot = new Snapshot(editorState) + return !!query && !snapshot.equals(query) + } +) + export const mustGetLake = createSelector(Lakes.raw, getLakeId, (lakes, id) => { if (!id) throw new Error("Current lake id is unset") if (!lakes[id]) throw new Error(`Missing lake id: ${id}`) @@ -162,11 +125,6 @@ export const getPools = createSelector(getLake, Pools.raw, (l, pools) => { export const getTabId = getActive -export const getSessionHistory = createSelector( - [getTabId, SessionHistories.raw], - (tabId, histories) => histories[tabId] -) - export const getSessionId = getTabId export function getOpEventContext(state: State) { diff --git a/apps/zui/src/js/state/Editor/selectors.ts b/apps/zui/src/js/state/Editor/selectors.ts index d4b86360b2..732b9e8684 100644 --- a/apps/zui/src/js/state/Editor/selectors.ts +++ b/apps/zui/src/js/state/Editor/selectors.ts @@ -1,5 +1,4 @@ -import {createSelector, nanoid} from "@reduxjs/toolkit" -import {QueryVersion} from "../QueryVersions/types" +import {createSelector} from "@reduxjs/toolkit" import activeTabSelect from "../Tab/activeTabSelect" export const getPins = activeTabSelect((tab) => { @@ -26,9 +25,7 @@ export const getSnapshot = activeTabSelect((tab) => { return { value: tab.editor.value, pins: tab.editor.pins, - version: nanoid(), - ts: new Date().toISOString(), - } as QueryVersion + } }) export const isEmpty = createSelector(getValue, getPins, (value, pins) => { diff --git a/apps/zui/src/js/state/Queries/helpers.test.ts b/apps/zui/src/js/state/Queries/helpers.test.ts index 1f503bd69f..33c69c2d82 100644 --- a/apps/zui/src/js/state/Queries/helpers.test.ts +++ b/apps/zui/src/js/state/Queries/helpers.test.ts @@ -9,22 +9,32 @@ const excludeTestQueries: Query[] = [ { id: "1", name: "Exclude me please", + pins: [], + value: "", }, { id: "2", name: "query #1", + pins: [], + value: "", }, { id: "3", name: "Query#1", + pins: [], + value: "", }, { id: "4", name: "Query #1s1", + pins: [], + value: "", }, { id: "5", name: "Query #1 ", + pins: [], + value: "", }, ] @@ -32,14 +42,20 @@ const includeTestQueries: Query[] = [ { id: "6", name: "Query #1", + pins: [], + value: "", }, { id: "7", name: "Query #3", + pins: [], + value: "", }, { id: "8", name: "Query #2", + pins: [], + value: "", }, ] @@ -52,7 +68,7 @@ test("getNextCount", () => { expect( getNextCount( [ - {id: "9", name: "Query #10"}, + {id: "9", name: "Query #10", pins: [], value: ""}, ...excludeTestQueries, ...includeTestQueries, ], diff --git a/apps/zui/src/js/state/Queries/parsers.ts b/apps/zui/src/js/state/Queries/parsers.ts index 0700489b14..555f57a37f 100644 --- a/apps/zui/src/js/state/Queries/parsers.ts +++ b/apps/zui/src/js/state/Queries/parsers.ts @@ -1,7 +1,6 @@ import {Group} from "./types" import {nanoid} from "@reduxjs/toolkit" -import {cloneDeep, last} from "lodash" -import {QueryVersion, QueryVersionsState} from "../QueryVersions/types" +import {cloneDeep} from "lodash" import {QueryPin} from "../Editor/types" import file from "src/js/lib/file" @@ -15,48 +14,29 @@ export type JSONGroup = { items: (JSONGroup | JSONQuery)[] } -export const parseJSONLib = ( - filePath: string -): {libRoot: Group; versions: {[queryId: string]: QueryVersion}} => { +export const parseJSONLib = (filePath: string): {libRoot: Group} => { const contents = file(filePath).readSync() const libRoot: Group = JSON.parse(contents) - const versions = {} flattenItemTree(libRoot).forEach((item) => { item.id = nanoid() if ("items" in item) { item.isOpen = false } else { - versions[item.id] = { - version: nanoid(), - ts: new Date().toISOString(), - value: item.value || "", - pins: [...(item.pins ?? [])], - } as QueryVersion - delete item.value - delete item.pins + item.value ??= "" + item.pins ??= [] } }) // The lib root is what gets added to queries - // The versions is an object keyed by the query id - // and the value is a single version to create - return {libRoot, versions} + return {libRoot} } -export const serializeQueryLib = ( - group: Group, - versions: QueryVersionsState -): JSONGroup => { +export const serializeQueryLib = (group: Group): JSONGroup => { // remove internal keys const jsonGroup = cloneDeep(group) const flat = flattenItemTree(jsonGroup) flat.forEach((item) => { if ("items" in item) return - const queryVs = versions[item.id] - if (!queryVs) return - const {value, pins} = queryVs.entities[last(queryVs.ids)] - item.value = value - item.pins = pins delete item.id delete item.isOpen }) @@ -64,7 +44,7 @@ export const serializeQueryLib = ( return jsonGroup as JSONGroup } -const flattenItemTree = (root) => { +export const flattenItemTree = (root) => { const items = [root] for (let i = 0; i < items.length; i++) { const current = items[i] diff --git a/apps/zui/src/js/state/Queries/queries.test.ts b/apps/zui/src/js/state/Queries/queries.test.ts index ee66fc25d7..e0cd05a7a6 100644 --- a/apps/zui/src/js/state/Queries/queries.test.ts +++ b/apps/zui/src/js/state/Queries/queries.test.ts @@ -28,6 +28,7 @@ const testLib = { // .items[0].items[0] id: "testId2", name: "testName2", + pins: [], description: "testDescription2", value: "testValue2", tags: ["testTag1", "testTag2"], @@ -43,6 +44,7 @@ const testLib = { name: "testName4", description: "testDescription4", value: "testValue4", + pins: [], tags: ["testTag2"], }, ], @@ -52,6 +54,7 @@ const testLib = { id: "testId5", name: "testName5", description: "testDescription5", + pins: [], value: "testValue5", tags: ["testTag1"], }, @@ -65,6 +68,7 @@ const newQuery = { name: "newQueryName", description: "newQueryDescription", value: "newQueryValue", + pins: [], tags: [], } diff --git a/apps/zui/src/js/state/Queries/selectors.ts b/apps/zui/src/js/state/Queries/selectors.ts index c60fed433e..6a0e08fae2 100644 --- a/apps/zui/src/js/state/Queries/selectors.ts +++ b/apps/zui/src/js/state/Queries/selectors.ts @@ -2,59 +2,15 @@ import {Group, QueriesState, Query} from "./types" import {State} from "../types" import TreeModel from "tree-model" import {createSelector} from "reselect" -import QueryVersions from "../QueryVersions" -import {QueryModel} from "src/js/models/query-model" -import {entitiesToArray} from "../utils" -import memoizeOne from "memoize-one" -import SessionQueries from "../SessionQueries" export const raw = (state: State): QueriesState => state.queries -export const find = (state: State, id: string): Query | null => { +export const find = (queries: QueriesState, id: string): Query | null => { return new TreeModel({childrenPropertyName: "items"}) - .parse(state.queries) + .parse(queries) .first((n) => n.model.id === id)?.model } -export const findSessionQuery = (state: State, id: string): Query | null => { - return state.sessionQueries[id] -} - -const memoGetVersions = memoizeOne(entitiesToArray) - -const getQueryVersions = (state: State, id: string) => { - const ids = QueryVersions.at(id).ids(state) - const entities = QueryVersions.at(id).entities(state) - return memoGetVersions(ids, entities) -} - -export const build = createSelector( - find, - findSessionQuery, - getQueryVersions, - (localMeta, sessionMeta, versions) => { - if (localMeta) return new QueryModel(localMeta, versions, "local") - if (sessionMeta) return new QueryModel(sessionMeta, versions, "session") - return null - } -) - -export const makeBuildSelector = () => { - return createSelector(find, getQueryVersions, (meta, versions) => { - if (!meta) return null - return new QueryModel(meta, versions, "local") - }) -} - -/** - * @deprecated use find instead - */ -export const getQueryById = - (queryId: string) => - (state: State): Query => { - return find(state, queryId) - } - export const getGroupById = (groupId: string) => (state: State): Group => { @@ -63,43 +19,6 @@ export const getGroupById = .first((n) => n.model.id === groupId && "items" in n.model)?.model } -export const getTags = createSelector( - raw, - (queries): string[] => { - const tagMap = {} - new TreeModel({childrenPropertyName: "items"}).parse(queries).walk((n) => { - // skip if it is group (true means continue) - if (!n.model.tags) return true - n.model.tags.forEach((t) => { - tagMap[t] = true - }) - - return true - }) - - return Object.keys(tagMap) - } -) - export const any = createSelector(getGroupById("root"), (group) => { return group.items.length > 0 }) - -export const getQueryIdToName = createSelector( - raw, - SessionQueries.raw, - (localRaw, sessionRaw) => { - const idNameMap = {} - Object.values(sessionRaw).forEach( - (session) => (idNameMap[session.id] = session.name) - ) - new TreeModel({childrenPropertyName: "items"}).parse(localRaw).walk((n) => { - if (!("items" in n.model)) { - idNameMap[n.model.id] = n.model.name - } - return true - }) - - return idNameMap - } -) diff --git a/apps/zui/src/js/state/Queries/test.ts b/apps/zui/src/js/state/Queries/test.ts deleted file mode 100644 index 04d773f4bd..0000000000 --- a/apps/zui/src/js/state/Queries/test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import initTestStore from "src/test/unit/helpers/initTestStore" -import Queries from "." -import QueryVersions from "../QueryVersions" -import {makeBuildSelector} from "./selectors" -import {Store} from "../types" - -let store: Store -beforeEach(async () => { - store = await initTestStore() -}) - -test("build selector", () => { - store.dispatch(Queries.addItem({name: "My Queriy", id: "1"})) - store.dispatch(Queries.addItem({name: "My Queriy", id: "2"})) - store.dispatch( - QueryVersions.at("1").create({ - ts: new Date().toISOString(), - version: "0.1", - value: "count()", - pins: [{type: "from", value: "mypool"}], - }) - ) - const build1 = makeBuildSelector() - const build2 = makeBuildSelector() - - const instance1 = build1(store.getState(), "1") - const instance2 = build2(store.getState(), "2") - const instance3 = build1(store.getState(), "1") - const instance4 = build2(store.getState(), "2") - expect(instance1).toBe(instance3) - expect(instance2).toBe(instance4) -}) diff --git a/apps/zui/src/js/state/Queries/types.ts b/apps/zui/src/js/state/Queries/types.ts index 863e2fb310..cd2942a894 100644 --- a/apps/zui/src/js/state/Queries/types.ts +++ b/apps/zui/src/js/state/Queries/types.ts @@ -1,12 +1,13 @@ +import {QueryPin} from "../Editor/types" + export type QueriesState = Group export type Item = Query | Group export interface Query { id: string name: string - tags?: string[] - description?: string - isReadOnly?: boolean + pins: QueryPin[] + value: string } export interface Group { diff --git a/apps/zui/src/js/state/QueryVersions/index.ts b/apps/zui/src/js/state/QueryVersions/index.ts deleted file mode 100644 index e2a2f2a0e7..0000000000 --- a/apps/zui/src/js/state/QueryVersions/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {isEqual} from "lodash" -import {State} from "../types" -import {versionSlice, reducer} from "./reducer" -import {QueryVersion} from "./types" - -export default { - ...versionSlice, - raw: (state: State) => state.queryVersions, - reducer, - areEqual(a: QueryVersion, b: QueryVersion) { - return isEqual(a?.pins, b?.pins) && isEqual(a?.value, b?.value) - }, - initial(): QueryVersion { - return { - ts: new Date().toISOString(), - version: "0", - value: "", - pins: [], - } - }, -} diff --git a/apps/zui/src/js/state/QueryVersions/query-versions.test.ts b/apps/zui/src/js/state/QueryVersions/query-versions.test.ts deleted file mode 100644 index f00fcb65b2..0000000000 --- a/apps/zui/src/js/state/QueryVersions/query-versions.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import QueryVersions from "." -import initTestStore from "../../../test/unit/helpers/initTestStore" -import {QueryVersion} from "./types" - -let store -beforeEach(async () => { - store = await initTestStore() -}) - -const testQueryId = "testQueryId" -const testQueryId2 = "testQueryId2" -const testVersion: QueryVersion = { - version: "v1.0.0", - ts: new Date(1).toISOString(), - value: "test zed", - pins: [], -} -const testVersion2: QueryVersion = { - version: "v2.0.0", - ts: new Date(2).toISOString(), - value: "test zed", - pins: [], -} - -test("add/delete versions", () => { - store.dispatch(QueryVersions.at(testQueryId).create(testVersion)) - const all = QueryVersions.at(testQueryId).all(store.getState()) - expect(all).toEqual([testVersion]) - store.dispatch(QueryVersions.at(testQueryId).create(testVersion2)) - expect(QueryVersions.at(testQueryId).all(store.getState())).toEqual([ - testVersion, - testVersion2, - ]) - store.dispatch(QueryVersions.at(testQueryId2).create(testVersion)) - expect( - QueryVersions.at(testQueryId2).find(store.getState(), testVersion.version) - ).toEqual(testVersion) - - store.dispatch(QueryVersions.at(testQueryId).delete(testVersion)) - expect(QueryVersions.at(testQueryId).all(store.getState())).toEqual([ - testVersion2, - ]) -}) diff --git a/apps/zui/src/js/state/QueryVersions/reducer.ts b/apps/zui/src/js/state/QueryVersions/reducer.ts deleted file mode 100644 index e9cacef7a8..0000000000 --- a/apps/zui/src/js/state/QueryVersions/reducer.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {createReducer, PayloadAction} from "@reduxjs/toolkit" -import { - createNestedEntitySlice, - initialState, -} from "../entity-slice/create-entity-slice" -import {State} from "../types" -import {QueryVersion} from "./types" - -type VersionMeta = {queryId: string} - -const initial = initialState() - -export const versionSlice = createNestedEntitySlice< - QueryVersion, - {queryId: string}, - [queryId: string] ->({ - name: "$version", - id: (v) => v.version, - sort: (a, b) => (a.ts > b.ts ? 1 : -1), - meta: (queryId) => ({queryId}), - select: (state: State, meta) => { - if (!state.queryVersions) return initial - return state.queryVersions[meta.queryId] ?? initial - }, -}) - -export const reducer = createReducer({}, (builder) => { - builder.addMatcher( - ({type}) => type.startsWith(versionSlice.name), - (state, action: PayloadAction) => { - const id = action.meta.queryId - state[id] = versionSlice.reducer(state[id], action) - if (!state[id].ids.length) delete state[id] - } - ) -}) diff --git a/apps/zui/src/js/state/QueryVersions/types.ts b/apps/zui/src/js/state/QueryVersions/types.ts deleted file mode 100644 index 35d89705a1..0000000000 --- a/apps/zui/src/js/state/QueryVersions/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {EntityState} from "@reduxjs/toolkit" -import {QueryPin} from "../Editor/types" - -export type VersionsState = EntityState -export type QueryVersionsState = { - [queryId: string]: VersionsState -} - -export type QueryVersion = { - version: string - ts: string - value: string - pins: QueryPin[] -} diff --git a/apps/zui/src/js/state/SessionHistories/flows.ts b/apps/zui/src/js/state/SessionHistories/flows.ts deleted file mode 100644 index 6a530758b7..0000000000 --- a/apps/zui/src/js/state/SessionHistories/flows.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {Thunk} from "../types" -import Current from "../Current" -import SessionHistories from "." -import Queries from "../Queries" - -export const push = - (queryId: string, versionId?: string): Thunk => - (dispatch, getState) => { - const sessionId = Current.getTabId(getState()) - const savedQuery = Queries.build(getState(), queryId) - const version = versionId || savedQuery?.latestVersionId() || "" - const entry = {queryId, version} - dispatch(SessionHistories.pushById({sessionId, entry})) - } - -export const replace = - (queryId: string, versionId?: string): Thunk => - (dispatch, getState) => { - const sessionId = Current.getTabId(getState()) - const savedQuery = Queries.build(getState(), queryId) - const version = versionId || savedQuery?.latestVersionId() || "" - const entry = {queryId, version} - dispatch(SessionHistories.replaceById({sessionId, entry})) - } diff --git a/apps/zui/src/js/state/SessionHistories/index.ts b/apps/zui/src/js/state/SessionHistories/index.ts deleted file mode 100644 index 8ba615604b..0000000000 --- a/apps/zui/src/js/state/SessionHistories/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {actions, reducer} from "./reducer" -import * as flows from "./flows" -import * as selectors from "./selectors" - -export default { - reducer, - ...flows, - ...actions, - ...selectors, -} diff --git a/apps/zui/src/js/state/SessionHistories/reducer.ts b/apps/zui/src/js/state/SessionHistories/reducer.ts deleted file mode 100644 index f48c46a03e..0000000000 --- a/apps/zui/src/js/state/SessionHistories/reducer.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {createSlice, PayloadAction} from "@reduxjs/toolkit" -import {SessionHistoriesState, SessionHistoryEntry} from "./types" - -const slice = createSlice({ - name: "$sessionHistories", - initialState: {} as SessionHistoriesState, - reducers: { - replaceById( - s, - a: PayloadAction<{sessionId: string; entry: SessionHistoryEntry}> - ) { - if (!s[a.payload.sessionId]) s[a.payload.sessionId] = [a.payload.entry] - else { - s[a.payload.sessionId].pop() - s[a.payload.sessionId].push(a.payload.entry) - } - }, - pushById( - s, - a: PayloadAction<{sessionId: string; entry: SessionHistoryEntry}> - ) { - if (!s[a.payload.sessionId]) s[a.payload.sessionId] = [a.payload.entry] - else s[a.payload.sessionId].push(a.payload.entry) - }, - deleteById(s, a: PayloadAction<{sessionId: string}>) { - delete s[a.payload.sessionId] - }, - deleteEntry(s, a: PayloadAction<{sessionId: string; index: number}>) { - const session = s[a.payload.sessionId] - if (session) { - session.splice(a.payload.index, 1) - } - }, - }, -}) - -export const reducer = slice.reducer -export const actions = slice.actions diff --git a/apps/zui/src/js/state/SessionHistories/selectors.ts b/apps/zui/src/js/state/SessionHistories/selectors.ts deleted file mode 100644 index 83abe19edc..0000000000 --- a/apps/zui/src/js/state/SessionHistories/selectors.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {SessionHistoriesState, SessionHistoryEntry} from "./types" - -export const raw = (s): SessionHistoriesState => s.sessionHistories -export const getById = - (sessionId: string) => - (s): SessionHistoryEntry[] => - s.sessionHistories[sessionId] diff --git a/apps/zui/src/js/state/SessionHistories/types.ts b/apps/zui/src/js/state/SessionHistories/types.ts deleted file mode 100644 index c1b961c015..0000000000 --- a/apps/zui/src/js/state/SessionHistories/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type SessionHistoryEntry = { - queryId: string - version: string -} - -export type SessionHistoriesState = { - [sessionId: string]: SessionHistoryEntry[] -} diff --git a/apps/zui/src/js/state/SessionQueries/flows.ts b/apps/zui/src/js/state/SessionQueries/flows.ts deleted file mode 100644 index 0fe3eda192..0000000000 --- a/apps/zui/src/js/state/SessionQueries/flows.ts +++ /dev/null @@ -1,5 +0,0 @@ -import SessionQueries from "." - -export const init = (id: string) => (dispatch) => { - dispatch(SessionQueries.set({id, name: "Query Session"})) -} diff --git a/apps/zui/src/js/state/SessionQueries/index.ts b/apps/zui/src/js/state/SessionQueries/index.ts deleted file mode 100644 index ebee3f4485..0000000000 --- a/apps/zui/src/js/state/SessionQueries/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {reducer, actions} from "./reducer" -import * as flows from "./flows" -import * as selectors from "./selectors" - -export default { - reducer, - ...flows, - ...actions, - ...selectors, -} diff --git a/apps/zui/src/js/state/SessionQueries/reducer.ts b/apps/zui/src/js/state/SessionQueries/reducer.ts deleted file mode 100644 index be921644ca..0000000000 --- a/apps/zui/src/js/state/SessionQueries/reducer.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {createSlice} from "@reduxjs/toolkit" - -const slice = createSlice({ - name: "$sessionQueries", - initialState: {}, - reducers: { - set(s, a) { - s[a.payload.id] = a.payload - }, - }, -}) - -export const reducer = slice.reducer -export const actions = slice.actions diff --git a/apps/zui/src/js/state/SessionQueries/selectors.ts b/apps/zui/src/js/state/SessionQueries/selectors.ts deleted file mode 100644 index de5bf30376..0000000000 --- a/apps/zui/src/js/state/SessionQueries/selectors.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {State} from "../types" -import {SessionQueriesState} from "./types" - -export const raw = (state: State): SessionQueriesState => state.sessionQueries -export const find = (state: State, id: string) => state.sessionQueries[id] diff --git a/apps/zui/src/js/state/SessionQueries/types.ts b/apps/zui/src/js/state/SessionQueries/types.ts deleted file mode 100644 index 9d417784cd..0000000000 --- a/apps/zui/src/js/state/SessionQueries/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {Query} from "../Queries/types" - -export type SessionQueriesState = { - [queryId: string]: Query -} diff --git a/apps/zui/src/js/state/Tab/reducer.ts b/apps/zui/src/js/state/Tab/reducer.ts index 19c27303b5..1986c28521 100644 --- a/apps/zui/src/js/state/Tab/reducer.ts +++ b/apps/zui/src/js/state/Tab/reducer.ts @@ -10,6 +10,10 @@ import {reducer as selection} from "../Selection/reducer" import {reducer as queryInfo} from "../QueryInfo/reducer" import {nanoid} from "@reduxjs/toolkit" +// The names of all actions sent to these reducers +// must start with "TAB_" so that it will route it +// to the active tab reducer. + const tabReducer = combineReducers({ editor, id: (state: string = nanoid(), _): string => state, @@ -24,6 +28,7 @@ const tabReducer = combineReducers({ table, selection, queryInfo, + title: (state = "Zui") => state, }) export type TabReducer = typeof tabReducer diff --git a/apps/zui/src/js/state/Tabs/find.ts b/apps/zui/src/js/state/Tabs/find.ts index 1e2352f812..e364e91964 100644 --- a/apps/zui/src/js/state/Tabs/find.ts +++ b/apps/zui/src/js/state/Tabs/find.ts @@ -1,7 +1,3 @@ -import {orderBy} from "lodash" -import {matchPath} from "react-router" -import {queryVersion} from "src/app/router/routes" - export function findTabByUrl(tabs, url) { return tabs.find((tab) => { return global.tabHistories.getOrCreate(tab.id).location.pathname === url @@ -11,11 +7,3 @@ export function findTabByUrl(tabs, url) { export function findTabById(tabs, id) { return tabs.find((tab) => tab.id === id) } - -export function findQuerySessionTab(tabs) { - return orderBy(tabs, ["lastFocused"], ["desc"]).find((tab) => { - const pathname = global.tabHistories.getOrCreate(tab.id).location.pathname - const match = matchPath(pathname, {path: queryVersion.path, exact: true}) - return !!match - }) -} diff --git a/apps/zui/src/js/state/Tabs/flows.ts b/apps/zui/src/js/state/Tabs/flows.ts index 7c49e8f34f..833d464788 100644 --- a/apps/zui/src/js/state/Tabs/flows.ts +++ b/apps/zui/src/js/state/Tabs/flows.ts @@ -1,15 +1,11 @@ import {nanoid} from "@reduxjs/toolkit" -import {queryPath} from "src/app/router/utils/paths" -import SessionQueries from "../SessionQueries" import {Thunk} from "../types" import Tabs from "./" import {findTabById, findTabByUrl} from "./find" -import {QuerySession} from "src/models/query-session" export const create = (url = "/", id = nanoid()): Thunk => (dispatch) => { - dispatch(SessionQueries.init(id)) dispatch(Tabs.add(id)) // move to tabHistories.restore(id, url) const history = global.tabHistories.get(id) @@ -23,18 +19,6 @@ export const create = return id } -export const createQuerySession = - (): Thunk => - (dispatch, getState, {api}) => { - const session = QuerySession.create() - const sessionId = session.id - const version = "0" - api.queries.createEditorSnapshot(sessionId, {version, value: "", pins: []}) - const url = queryPath(sessionId, version) - - return dispatch(create(url, sessionId)) - } - export const previewUrl = (url: string): Thunk => (dispatch, getState) => { diff --git a/apps/zui/src/js/state/Tabs/get-display-props.ts b/apps/zui/src/js/state/Tabs/get-display-props.ts new file mode 100644 index 0000000000..46f950adfd --- /dev/null +++ b/apps/zui/src/js/state/Tabs/get-display-props.ts @@ -0,0 +1,12 @@ +import {createSelector} from "@reduxjs/toolkit" +import {getData} from "./selectors" +import {createIsEqualSelector} from "../utils" + +export const getIdAndTitle = createSelector(getData, (tabs) => { + return tabs.map((tab) => ({id: tab.id, title: tab.title})) +}) + +export const getTabDisplayProps = createIsEqualSelector( + getIdAndTitle, + (props) => props +) diff --git a/apps/zui/src/js/state/Tabs/get-tab-models.ts b/apps/zui/src/js/state/Tabs/get-tab-models.ts deleted file mode 100644 index 70434890f7..0000000000 --- a/apps/zui/src/js/state/Tabs/get-tab-models.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {createSelector} from "@reduxjs/toolkit" -import {getIds} from "./selectors" -import Pools from "../Pools" -import Lakes from "../Lakes" -import Current from "../Current" -import Queries from "../Queries" -import tab from "src/js/models/tab" - -// We need to store the title of the tab in a different way -export const getTabModels = createSelector( - getIds, - Pools.raw, - Lakes.raw, - Current.getLakeId, - Queries.getQueryIdToName, - Current.getLocation, // So that it re-renders on start - (ids, pools, lakes, lakeId, queryIdToName) => { - return ids.map((id) => tab(id, lakes, pools, queryIdToName, lakeId)) - } -) diff --git a/apps/zui/src/js/state/Tabs/reducer.ts b/apps/zui/src/js/state/Tabs/reducer.ts index bcdd1c7b18..c51ef00991 100644 --- a/apps/zui/src/js/state/Tabs/reducer.ts +++ b/apps/zui/src/js/state/Tabs/reducer.ts @@ -65,6 +65,10 @@ const slice = createSlice({ const tab = findTab(s, s.active) tab.lastLocationKey = a.payload }, + setTitle(s, a: PayloadAction<{tabId: string; title: string}>) { + const tab = findTab(s, a.payload.tabId) + tab.title = a.payload.title + }, }, extraReducers: (builder) => { builder.addMatcher(isTabAction, (s, a: any) => { diff --git a/apps/zui/src/js/state/Tabs/selectors.ts b/apps/zui/src/js/state/Tabs/selectors.ts index c1956e2a0b..45c694fb15 100644 --- a/apps/zui/src/js/state/Tabs/selectors.ts +++ b/apps/zui/src/js/state/Tabs/selectors.ts @@ -1,7 +1,4 @@ import {createSelector} from "reselect" -import {State} from "../types" -import {createIsEqualSelector} from "../utils" -import {findQuerySessionTab} from "./find" import {activeTabsSelect, getActiveTabs} from "../Window/selectors" export const getData = activeTabsSelect((tabs) => tabs.data) @@ -16,19 +13,5 @@ export const getActiveTab = createSelector(getActiveTabs, (tabs) => { return tab }) -export const _getIds = createSelector(getData, (data) => { - return data.map((d) => d.id) -}) - -export const getIds = createIsEqualSelector( - _getIds, - (ids) => ids -) - -export const findFirstQuerySession = createSelector( - getData, - findQuerySessionTab -) - export const findById = (tabId: string) => createSelector(getData, (tabs) => tabs.find((t) => t.id === tabId)) diff --git a/apps/zui/src/js/state/migrations/202409271055_migrateToSnapshots.test.ts b/apps/zui/src/js/state/migrations/202409271055_migrateToSnapshots.test.ts new file mode 100644 index 0000000000..cad2cbc54a --- /dev/null +++ b/apps/zui/src/js/state/migrations/202409271055_migrateToSnapshots.test.ts @@ -0,0 +1,6 @@ +import {migrate} from "src/test/unit/helpers/migrate" + +test("migrating 202409271055_migrateToSnapshots", async () => { + const _next = await migrate({state: "v1.18.0", to: "202409271055"}) + /* Create the assertions here */ +}) diff --git a/apps/zui/src/js/state/migrations/202409271055_migrateToSnapshots.ts b/apps/zui/src/js/state/migrations/202409271055_migrateToSnapshots.ts new file mode 100644 index 0000000000..884c9bbae1 --- /dev/null +++ b/apps/zui/src/js/state/migrations/202409271055_migrateToSnapshots.ts @@ -0,0 +1,148 @@ +import {matchPath} from "react-router" +import {getAllRendererStates, getGlobalState} from "./utils/getTestState" + +function last(array) { + return array[array.length - 1] +} + +export default function migrateToSnapshots(appState: any) { + /* Main Global State */ + const state = getGlobalState(appState) + + /* Query Versions */ + const versions = state.queryVersions + + /* Queries */ + function lastContents(versions) { + const lastId = last(versions.ids) + const {pins, value} = versions.entities[lastId] + return {pins, value} + } + + function isFolder(node) { + return Array.isArray(node.items) + } + + function visitNode(node) { + if (isFolder(node)) { + node.items.forEach(visitNode) + } else { + const contents = lastContents(versions[node.id]) + node.pins = contents.pins + node.value = contents.value + queries[node.id] = node + } + } + + /* Attach pins and value to the Queries */ + const queries = {} + visitNode(state.queries) + + /* Collect Snapshots From Session History */ + let snapshots = [] + for (const [sessionId, entries] of Object.entries(state.sessionHistories)) { + for (const entry of entries as any) { + const isNamed = !!queries[entry.queryId] + const version = entry.version + const editor = state.queryVersions[sessionId].entities[version] + const snapshot = { + createdAt: editor.ts, + updatedAt: editor.ts, + id: editor.version, + pins: editor.pins, + value: editor.value, + sessionId: sessionId, + queryId: isNamed ? entry.queryId : null, + } + snapshots.push(snapshot) + } + } + + /* Now Migrate the Paths in the Tab Histories */ + for (const win of getAllRendererStates(appState)) { + if (!win.tabHistories) continue + const histories = win.tabHistories.entities + for (const sessionId in histories as any) { + const entity = histories[sessionId] + for (let i = 0; i < entity.entries.length; i++) { + const path = entity.entries[i] + + const match = matchPath(path, { + path: "/queries/:queryId/versions/:versionId", + exact: true, + }) + /* Find the snapshot */ + if (match) { + const {queryId, versionId} = match.params as any + let snapshot = snapshots.find((s) => s.id == versionId) + if (!snapshot) { + /* The Snapshot is missing because it has invalid syntax */ + /* But it is stored in the queryVersions */ + const version = state.queryVersions[queryId].entities[versionId] + const isNamed = !!queries[queryId] + snapshot = { + createdAt: version.ts, + updatedAt: version.ts, + id: version.version, + pins: version.pins, + value: version.value, + sessionId: sessionId, + queryId: isNamed ? queryId : null, + } + snapshots.push(snapshot) + } + /* Re-write the path */ + const newPath = `/snapshots/${snapshot.id}` + entity.entries[i] = newPath + } + } + // Remove the nulls + entity.entries = compact(entity.entries) + // Set the index to the last one + entity.index = entity.entries.length - 1 + } + } + + /* Add the snapshots to the state */ + snapshots = uniqById(snapshots) + snapshots = sortByCreatedAt(snapshots) + state.snapshots = toEntityState(snapshots) + + /* Delete everything else from the state */ + delete state.sessionQueries + delete state.sessionHistories + delete state.queryVersions + + return appState +} + +function uniqById(array) { + let ids = {} + let result = [] + for (const item of array) { + if (ids[item.id]) continue + ids[item.id] = true + result.push(item) + } + return result +} + +function sortByCreatedAt(array) { + return array.sort((a, b) => { + if (a.createdAt < b.createdAt) return -1 + else return 1 + }) +} + +function toEntityState(array) { + const slice = {ids: [], entities: {}} + for (const item of array) { + slice.ids.push(item.id) + slice.entities[item.id] = item + } + return slice +} + +function compact(array) { + return array.filter((item) => !!item) +} diff --git a/apps/zui/src/js/state/migrations/index.ts b/apps/zui/src/js/state/migrations/index.ts index 388a46e639..8e56c2db0d 100644 --- a/apps/zui/src/js/state/migrations/index.ts +++ b/apps/zui/src/js/state/migrations/index.ts @@ -45,3 +45,4 @@ export * as v202302161437 from "./202302161437_addLayoutDefaults" export * as v202307101053 from "./202307101053_migrateLakeTabs" export * as v202307141454 from "./202307141454_moveSecondarySidebarState" export * as v202407221450 from "./202407221450_populateSessions" +export * as v202409271055 from "./202409271055_migrateToSnapshots" diff --git a/apps/zui/src/js/state/migrations/utils/getTestState.ts b/apps/zui/src/js/state/migrations/utils/getTestState.ts index 9d2d12235a..7e371a9d39 100644 --- a/apps/zui/src/js/state/migrations/utils/getTestState.ts +++ b/apps/zui/src/js/state/migrations/utils/getTestState.ts @@ -38,6 +38,10 @@ export function getAllRendererStates(sessionState: SessionState): any[] { return compact(states) } +export function getGlobalState(sessionState: SessionState): any { + return sessionState.globalState +} + export function getAllTabs_before_202307101053( sessionState: SessionState ): any[] { diff --git a/apps/zui/src/js/state/stores/get-persistable.ts b/apps/zui/src/js/state/stores/get-persistable.ts index 697aa3f813..5a248afb63 100644 --- a/apps/zui/src/js/state/stores/get-persistable.ts +++ b/apps/zui/src/js/state/stores/get-persistable.ts @@ -11,11 +11,9 @@ export const GLOBAL_PERSIST: StateKey[] = [ "lakes", "launches", "queries", - "queryVersions", - "sessionQueries", "poolSettings", "querySessions", - "sessionHistories", + "snapshots", ] export const WINDOW_PERSIST: StateKey[] = [ @@ -24,7 +22,13 @@ export const WINDOW_PERSIST: StateKey[] = [ "window", ] -export const TAB_PERSIST: TabKey[] = ["editor", "id", "lastFocused", "layout"] +export const TAB_PERSIST: TabKey[] = [ + "editor", + "id", + "lastFocused", + "layout", + "title", +] export function getPersistedWindowState(original?: State) { if (!original) return diff --git a/apps/zui/src/js/state/stores/root-reducer.ts b/apps/zui/src/js/state/stores/root-reducer.ts index c0405e9f3c..69a8a47772 100644 --- a/apps/zui/src/js/state/stores/root-reducer.ts +++ b/apps/zui/src/js/state/stores/root-reducer.ts @@ -14,14 +14,12 @@ import ConfigPropValues from "../ConfigPropValues" import Launches from "../Launches" import Appearance from "../Appearance" import Loads from "../Loads" -import QueryVersions from "../QueryVersions" -import SessionQueries from "../SessionQueries" -import SessionHistories from "../SessionHistories" import PoolSettings from "../PoolSettings" import Window from "../Window" import LoadDataForm from "../LoadDataForm" import Updates from "../Updates" import {QuerySession} from "src/models/query-session" +import {Snapshot} from "src/models/snapshot" const rootReducer = combineReducers({ appearance: Appearance.reducer, @@ -37,18 +35,13 @@ const rootReducer = combineReducers({ pools: Pools.reducer, poolSettings: PoolSettings.reducer, queries: Queries.reducer, - queryVersions: QueryVersions.reducer, - sessionHistories: SessionHistories.reducer, - sessionQueries: SessionQueries.reducer, tabHistories: TabHistories.reducer, toolbars: Toolbars.reducer, url: Url.reducer, window: Window.reducer, updates: Updates.reducer, ...QuerySession.slice, + ...Snapshot.slice, }) -// A proof of concept. This would be a much nicer way to go -// once we have time to convert to it. -// type RootState = ReturnType export default rootReducer diff --git a/apps/zui/src/js/state/types.ts b/apps/zui/src/js/state/types.ts index 71d8c61a54..3ed8948a9f 100644 --- a/apps/zui/src/js/state/types.ts +++ b/apps/zui/src/js/state/types.ts @@ -14,15 +14,13 @@ import {QueriesState} from "./Queries/types" import {TabHistoriesState} from "./TabHistories/types" import {ToolbarsState} from "./Toolbars" 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" import {WindowState} from "./Window/types" import {LoadDataFormState} from "./LoadDataForm/types" import {UpdatesState} from "./Updates/types" import {EnhancedStore} from "@reduxjs/toolkit" import {QuerySessionState} from "src/models/query-session" +import {SnapshotsState} from "src/models/snapshot" export type ThunkExtraArg = { api: ZuiApi @@ -50,12 +48,10 @@ export type State = { pools: PoolsState poolSettings: PoolSettingsState queries: QueriesState - queryVersions: QueryVersionsState - sessionHistories: SessionHistoriesState - sessionQueries: SessionQueriesState tabHistories: TabHistoriesState toolbars: ToolbarsState window: WindowState updates: UpdatesState querySessions: QuerySessionState + snapshots: SnapshotsState } diff --git a/apps/zui/src/models/active-query.ts b/apps/zui/src/models/active-query.ts deleted file mode 100644 index f3d43e39f9..0000000000 --- a/apps/zui/src/models/active-query.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {QueryModel} from "src/js/models/query-model" -import {QueryVersion} from "src/js/state/QueryVersions/types" -import {EditorSnapshot} from "src/models/editor-snapshot" - -export class ActiveQuery { - constructor( - public session: QueryModel, // the singleton for the tab - public query: QueryModel | null, // the query from the url param - public version: QueryVersion | null // the version from the url param - ) {} - - id() { - return this.query?.id || this.session.id - } - - versionId() { - return this.version?.version || null - } - - isDeleted() { - return !this.query && !this.version - } - - isAnonymous() { - return !this.query || this.query.id === this.session.id - } - - isSaved() { - return !this.isAnonymous() - } - - isLatest() { - return ( - !this.isAnonymous() && this.query.latestVersionId() === this.versionId() - ) - } - - isModified() { - return !this.isAnonymous() && !this.isLatest() - } - - isOutdated() { - return !this.isAnonymous() && !this.isModified() && !this.isLatest() - } - - isReadOnly() { - return this.isSaved() && !!this.query.isReadOnly - } - - name() { - if (this.isAnonymous()) return "" - return this.query.name - } - - value() { - return this.version?.value - } - - ts() { - return this.version?.ts - } - - toZed() { - return new EditorSnapshot(this.version).toQueryText() - } -} diff --git a/apps/zui/src/models/active.ts b/apps/zui/src/models/active.ts index bfec5d82ae..f3f5ee0a5b 100644 --- a/apps/zui/src/models/active.ts +++ b/apps/zui/src/models/active.ts @@ -1,12 +1,12 @@ import {DomainModel} from "src/core/domain-model" -import {Session} from "./session" import Current from "src/js/state/Current" import {BrowserTab} from "./browser-tab" import {Frame} from "./frame" import {getActiveTab} from "src/js/state/Tabs/selectors" -import Editor from "src/js/state/Editor" import {Lake} from "./lake" -import {EditorSnapshot} from "./editor-snapshot" +import {Snapshot} from "./snapshot" +import {QuerySession} from "./query-session" +import Editor from "src/js/state/Editor" export class Active extends DomainModel { static get tab() { @@ -14,22 +14,18 @@ export class Active extends DomainModel { return new BrowserTab({id, lastFocused}) } - static get session() { - const params = this.select(Current.getQueryUrlParams) - return new Session({ - id: this.tab.attrs.id, - parentId: params.queryId, - snapshotId: params.version, - }) + static get querySession() { + const {id} = this.select(getActiveTab) + return QuerySession.find(id) } - static get snapshot() { - const params = this.select(Current.getQueryUrlParams) + static get editorState() { + return this.select(Editor.getSnapshot) + } - return new EditorSnapshot({ - parentId: params.queryId, - ...this.select(Editor.getSnapshot), - }) + static get snapshot() { + const id = this.select(Current.getSnapshotId) + return Snapshot.find(id) } static get frame() { diff --git a/apps/zui/src/models/browser-tab.ts b/apps/zui/src/models/browser-tab.ts index d2f1f1b5ee..7c3e1f9350 100644 --- a/apps/zui/src/models/browser-tab.ts +++ b/apps/zui/src/models/browser-tab.ts @@ -4,6 +4,11 @@ import {matchPath} from "react-router" import {DomainModel} from "src/core/domain-model" import Tabs from "src/js/state/Tabs" import {QuerySession} from "./query-session" +import {whichRoute} from "src/app/router/routes" +import {IconName} from "src/components/icon" +import {Snapshot} from "./snapshot" +import Pools from "src/js/state/Pools" +import Current from "src/js/state/Current" type Attrs = { id: string @@ -11,6 +16,16 @@ type Attrs = { } export class BrowserTab extends DomainModel { + get id() { + return this.attrs.id + } + + static findByRoute(routePattern: string) { + return this.orderBy("lastFocused", "desc").find((tab) => + tab.matchesPath(routePattern) + ) + } + static find(id: string) { const attrs = this.select(Tabs.findById(id)) return attrs ? new BrowserTab(attrs) : null @@ -61,6 +76,10 @@ export class BrowserTab extends DomainModel { } } + reload() { + this.history.replace(this.history.location.pathname) + } + activate() { this.dispatch(Tabs.activate(this.attrs.id)) } @@ -78,6 +97,60 @@ export class BrowserTab extends DomainModel { if (session && session.history.length === 0) { session.destroy() } + this.remove() + } + + get route() { + return whichRoute(this.history.location.pathname) + } + + get iconName(): IconName { + return this.route?.icon || "zui" + } + + setTitle(title: string) { + this.dispatch(Tabs.setTitle({tabId: this.attrs.id, title})) + } + + remove() { this.dispatch(Tabs.remove(this.attrs.id)) } + + get params(): any { + return matchPath(this.pathname, this.route.path).params ?? {} + } + + get pathname() { + return this.history.location.pathname + } + + updateTitle() { + // Not the prettiest of code + const lakeId = this.select(Current.getLakeId) + switch (this.route.name) { + case "snapshot": + var id = this.params.id + var snapshot = Snapshot.find(id) + this.setTitle(snapshot?.title || "Query Session") + break + case "poolShow": + var id = this.params.poolId + var pool = this.select(Pools.get(lakeId, id)) + if (pool) { + this.setTitle(pool.name) + } else { + this.setTitle("Pool") + } + break + case "welcome": + this.setTitle("Welcome") + break + case "releaseNotes": + this.setTitle("Release Notes") + break + case "root": + // not sure yet + break + } + } } diff --git a/apps/zui/src/models/editor-snapshot.ts b/apps/zui/src/models/editor-snapshot.ts deleted file mode 100644 index 089a44d646..0000000000 --- a/apps/zui/src/models/editor-snapshot.ts +++ /dev/null @@ -1,98 +0,0 @@ -import {nanoid} from "@reduxjs/toolkit" -import {isEqual} from "lodash" -import {queryPath} from "src/app/router/utils/paths" -import {DomainModel} from "src/core/domain-model" -import buildPin from "src/js/state/Editor/models/build-pin" -import {QueryPin, QueryPinInterface} from "src/js/state/Editor/types" -import QueryVersions from "src/js/state/QueryVersions" -import {SourceSet} from "./editor-snapshot/source-set" -import {Validator} from "./editor-snapshot/validator" - -type Attrs = { - version: string - ts: string // aka lastRanAt - parentId: string | null - value: string - pins: QueryPin[] -} - -export class EditorSnapshot extends DomainModel { - validator = new Validator() - - constructor(attrs: Partial = {}) { - super({ - version: nanoid(), - ts: new Date().toISOString(), - value: "", - pins: [], - parentId: null, - ...attrs, - }) - } - - static find(parentId: string, id: string) { - const attrs = this.select((state) => - QueryVersions.at(parentId).find(state, id) - ) - return attrs ? new EditorSnapshot({...attrs, parentId}) : null - } - - static where(args: {parentId: string}) { - return this.select(QueryVersions.at(args.parentId).all).map( - (attrs) => new EditorSnapshot({...attrs, parentId: args.parentId}) - ) - } - - get id() { - return this.attrs.version - } - - get pathname() { - return queryPath(this.attrs.parentId, this.attrs.version) - } - - get parentId() { - return this.attrs.parentId - } - - activePins() { - return this.attrs.pins - .filter((pin) => !pin.disabled) - .map((attrs) => buildPin(attrs)) - } - - toSourceSet() { - return new SourceSet( - this.activePins().map((pin) => pin.toZed()), - this.attrs.value - ) - } - - toQueryText() { - return this.toSourceSet().contents - } - - equals(other: EditorSnapshot) { - return ( - isEqual(this.attrs.pins, other.attrs.pins) && - isEqual(this.attrs.value, other.attrs.value) - ) - } - - save() { - const {parentId, ...rest} = this.attrs - this.dispatch(QueryVersions.at(parentId).create({...rest})) - } - - clone(attrs: Partial) { - return new EditorSnapshot({...this.attrs, ...attrs}) - } - - async isValid() { - return this.validator.validate(this) - } - - get errors() { - return this.validator.errors - } -} diff --git a/apps/zui/src/models/named-query.ts b/apps/zui/src/models/named-query.ts index 44fdef3715..3fc9f40843 100644 --- a/apps/zui/src/models/named-query.ts +++ b/apps/zui/src/models/named-query.ts @@ -1,29 +1,58 @@ import {DomainModel} from "src/core/domain-model" import Queries from "src/js/state/Queries" -import {EditorSnapshot} from "./editor-snapshot" +import {nanoid} from "@reduxjs/toolkit" +import {QueryPin} from "src/js/state/Editor/types" +import {Query} from "src/js/state/Queries/types" +import {Snapshot} from "./snapshot" type Attrs = { name: string id: string + value: string + pins: QueryPin[] } export class NamedQuery extends DomainModel { static find(id: string) { - const attrs = this.select((state) => Queries.find(state, id)) + const attrs = this.select((state) => Queries.find(state.queries, id)) if (!attrs) return null - const {name} = attrs - return new NamedQuery({id, name}) + return new NamedQuery(attrs) + } + + static create(args: { + name: string + pins: QueryPin[] + value: string + parentId?: string + }) { + const id = nanoid() + const {name, pins, value, parentId} = args + const attrs = {id, pins, value, name} + this.dispatch(Queries.addItem(attrs, parentId)) + return this.find(id) } get id() { return this.attrs.id } + get pins() { + return this.attrs.pins + } + + get value() { + return this.attrs.value + } + get lastSnapshot() { return this.snapshots[this.snapshots.length - 1] } get snapshots() { - return EditorSnapshot.where({parentId: this.attrs.id}) + return Snapshot.where({queryId: this.attrs.id}) + } + + update(changes: Partial) { + this.dispatch(Queries.editItem({id: this.id, changes})) } } diff --git a/apps/zui/src/models/query-session.ts b/apps/zui/src/models/query-session.ts index 86cd1355a3..212939af83 100644 --- a/apps/zui/src/models/query-session.ts +++ b/apps/zui/src/models/query-session.ts @@ -3,15 +3,14 @@ import {ApplicationEntity} from "./application-entity" import {EntityState} from "@reduxjs/toolkit" import {BrowserTab} from "./browser-tab" import Tabs from "src/js/state/Tabs" -import {actions} from "src/js/state/SessionHistories/reducer" -import {getById} from "src/js/state/SessionHistories/selectors" -import {queryPath} from "src/app/router/utils/paths" import {last} from "lodash" -import {EditorSnapshot} from "./editor-snapshot" import cmd from "src/cmd" +import {Snapshot, SnapshotAttrs} from "./snapshot" +import {snapshotShow} from "src/app/router/routes" const schema = { name: {type: String, default: null as string}, + title: {type: String, default: null as string}, } type Attributes = AttributeTypes @@ -23,60 +22,116 @@ export class QuerySession extends ApplicationEntity { static actionPrefix = "$querySessions" static sliceName = "querySessions" + static load(snapshot: Snapshot) { + const session = this.activateOrCreate() + session.load(snapshot) + } + + static findLastFocused() { + const tab = BrowserTab.findByRoute(snapshotShow.path) + return tab ? this.find(tab.id) : null + } + + static activateOrCreate() { + const session = this.findLastFocused() || this.createWithTab() + session.activate() + return session + } + + static createWithTab() { + const instance = this.create() + instance.createTab() + return instance + } + + static createAndActivate() { + const session = this.createWithTab() + session.activate() + session.navigate({value: "", pins: []}) + } + + createTab() { + if (this.tab) return + return BrowserTab.create({ + id: this.id, + lastFocused: new Date().toISOString(), + }) + } + activate() { if (this.tab) this.tab.activate() else this.restore() } + /* Navigate creates a new snapshot based on the last one */ + navigate(attrs: Partial) { + const next = this.lastSnapshot + ? this.lastSnapshot.clone(attrs) + : new Snapshot({sessionId: this.id, ...attrs}) + next.save() + this.load(next) + } + + /* Load is used when you already have a saved snapshot */ + load(snapshot: Snapshot) { + this.update({title: snapshot.title}) + this.tab.setTitle(snapshot.title) + this.tab.load(snapshot.pathname) + } + + /* Reload */ + reload() { + this.tab.reload() + } + get tab() { return BrowserTab.find(this.id) } get history() { - return this.select(getById(this.id)) || [] - } - - get lastSnapshot() { - const entry = last(this.history) - if (!entry) return null - return EditorSnapshot.find(entry.queryId, entry.version) + return Snapshot.where({sessionId: this.id}) } get displayName() { - const snapshot = this.lastSnapshot - if (snapshot) return snapshot.toQueryText() - else return "(New Session)" + return ( + this.attributes.name || + this.attributes.title || + this.lastSnapshot?.queryText || + "(New Session)" + ) } get isActive() { return this.select(Tabs.getActive) === this.id } + get lastSnapshot() { + return last(this.history) + } + restore() { - const history = this.history - const entry = history && last(history) - if (entry) { - const url = queryPath(entry.queryId, entry.version) - this.dispatch(Tabs.create(url, this.id)) + this.createTab() + this.tab.activate() + const prev = this.lastSnapshot + if (prev) { + this.load(prev) } else { - const snapshot = new EditorSnapshot({ + this.navigate({ pins: [], value: "", - parentId: this.id, + sessionId: this.id, }) - snapshot.save() - this.dispatch(Tabs.create(snapshot.pathname, this.id)) } } destroy() { super.destroy() - this.dispatch(actions.deleteById({sessionId: this.id})) + this.history.forEach((snapshot) => snapshot.destroy()) global.tabHistories.delete(this.id) if (this.isActive) { cmd.tabs.closeActive() } else { - this.dispatch(Tabs.remove(this.id)) + this.tab.remove() } } } diff --git a/apps/zui/src/models/session-history.ts b/apps/zui/src/models/session-history.ts deleted file mode 100644 index aca03678af..0000000000 --- a/apps/zui/src/models/session-history.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {DomainModel} from "src/core/domain-model" -import SessionHistories from "src/js/state/SessionHistories" - -type Attrs = { - id: string -} - -export class SessionHistory extends DomainModel { - get id() { - return this.attrs.id - } - - push(parentId: string, snapshotId: string) { - this.dispatch( - SessionHistories.pushById({ - sessionId: this.attrs.id, - entry: { - queryId: parentId, - version: snapshotId, - }, - }) - ) - } - - get entries() { - return this.select(SessionHistories.getById(this.id)) || [] - } - - contains(parentId: string, snapshotId: string) { - return !!this.entries.find( - (item) => item.queryId === parentId && item.version === snapshotId - ) - } -} diff --git a/apps/zui/src/models/session.ts b/apps/zui/src/models/session.ts deleted file mode 100644 index 0f07cebb70..0000000000 --- a/apps/zui/src/models/session.ts +++ /dev/null @@ -1,126 +0,0 @@ -import {queryPath} from "src/app/router/utils/paths" -import {DomainModel} from "src/core/domain-model" -import Inspector from "src/js/state/Inspector" -import Table from "src/js/state/Table" -import Selection from "src/js/state/Selection" -import {EditorSnapshot} from "./editor-snapshot" -import {SessionHistory} from "./session-history" -import {NamedQuery} from "./named-query" -import {BrowserTab} from "./browser-tab" -import SessionQueries from "src/js/state/SessionQueries" -import {queryVersion} from "src/app/router/routes" -import {QuerySession} from "./query-session" - -type Attrs = { - id: string - parentId?: string - snapshotId?: string -} - -export class Session extends DomainModel { - static activateLastFocused() { - const tab = BrowserTab.orderBy("lastFocused", "desc").find((tab) => - tab.matchesPath(queryVersion.path) - ) - if (tab) { - tab.activate() - } else { - Session.create().tab.activate() - } - } - - static create() { - const {id} = QuerySession.create() - const now = new Date().toISOString() - this.dispatch(SessionQueries.init(id)) - BrowserTab.create({id, lastFocused: now}) - return new Session({id}) - } - - get hasUrl() { - return !!this.parentId && !!this.snapshotId - } - - get id() { - return this.attrs.id - } - - get parentId() { - return this.attrs.parentId - } - - get snapshotId() { - return this.attrs.snapshotId - } - - get pathname() { - return queryPath(this.parentId, this.snapshotId) - } - - get snapshot() { - let snapshot = EditorSnapshot.find(this.id, this.snapshotId) - if (snapshot) { - return snapshot - } else { - console.warn( - "Did not find snapshot on the session, falling back to named query snapshot" - ) - return EditorSnapshot.find(this.attrs.parentId, this.attrs.snapshotId) // remove after some time has gone by - } - } - - get snapshots() { - return EditorSnapshot.where({parentId: this.id}) - } - - get history() { - return new SessionHistory({id: this.id}) - } - - get hasNamedQuery() { - return this.parentId && this.id !== this.parentId - } - - get namedQuery() { - return this.hasNamedQuery ? NamedQuery.find(this.parentId) : null - } - - get isModified() { - return ( - this.hasNamedQuery && - !!this.namedQuery && - this.snapshot.equals(this.namedQuery.lastSnapshot) - ) - } - - get tab() { - return BrowserTab.find(this.id) - } - - navigate(snapshot: EditorSnapshot, namedQuery?: NamedQuery) { - const sessionSnapshot = snapshot.clone({parentId: this.id}) - sessionSnapshot.save() - new Session({ - id: this.id, - parentId: namedQuery ? namedQuery.id : this.id, - snapshotId: sessionSnapshot.id, - }).load() - } - - load() { - this.reset() - this.tab.load(this.pathname) - } - - pushHistory() { - if (!this.history.contains(this.parentId, this.snapshotId)) { - this.history.push(this.parentId, this.snapshotId) - } - } - - reset() { - this.dispatch(Selection.reset()) - this.dispatch(Table.setScrollPosition({top: 0, left: 0})) - this.dispatch(Inspector.setScrollPosition({top: 0, left: 0})) - } -} diff --git a/apps/zui/src/models/snapshot.ts b/apps/zui/src/models/snapshot.ts new file mode 100644 index 0000000000..992bdf1000 --- /dev/null +++ b/apps/zui/src/models/snapshot.ts @@ -0,0 +1,98 @@ +import {AttributeTypes} from "bullet" +import {ApplicationEntity} from "./application-entity" +import {snapshotPath} from "src/app/router/utils/paths" +import {QueryPin} from "src/js/state/Editor/types" +import buildPin from "src/js/state/Editor/models/build-pin" +import {SourceSet} from "./snapshot/source-set" +import {Validator} from "./snapshot/validator" +import {isEqual} from "lodash" +import Queries from "src/js/state/Queries" +import {EntityState} from "@reduxjs/toolkit" + +/* Schema */ +const schema = { + value: {type: String, default: ""}, + pins: {type: Array, default: []}, // Maybe use a serializable pin class + sessionId: {type: String, default: null}, + queryId: {type: String, default: null}, +} + +/* Types */ +export type SnapshotAttrs = AttributeTypes +export type SnapshotsState = EntityState + +/* Model */ +export class Snapshot extends ApplicationEntity { + /* Configuration */ + static schema = schema + static actionPrefix = "$snapshots" + + /* Attributes */ + value: string + pins: QueryPin[] + sessionId: string + queryId: string + + /* Instance Methods */ + clone(attrs: Partial) { + return new Snapshot({ + value: this.value, + pins: this.pins, + sessionId: this.sessionId, + queryId: this.queryId, + ...attrs, + }) + } + + get pathname() { + return snapshotPath(this.id) + } + + get queryText() { + return this.sourceSet.contents + } + + get activePins() { + return this.pins + .filter((pin) => !pin.disabled) + .map((attrs) => buildPin(attrs)) + } + + get sourceSet() { + return new SourceSet( + this.activePins.map((pin) => pin.toZed()), + this.value + ) + } + + validator = new Validator() + async isValid() { + return this.validator.validate(this) + } + + get errors() { + return this.validator.errors + } + + equals(other: {pins: QueryPin[]; value: string}) { + return isEqual(this.pins, other.pins) && isEqual(this.value, other.value) + } + + get query() { + return this.select((state) => Queries.find(state.queries, this.queryId)) + } + + get isSaved() { + return !!this.query + } + + get isEmpty() { + return this.equals({pins: [], value: ""}) + } + + get title() { + if (this.isEmpty) return "Query Session" + if (this.isSaved) return this.query.name + return this.queryText + } +} diff --git a/apps/zui/src/models/editor-snapshot/compilation-error.ts b/apps/zui/src/models/snapshot/compilation-error.ts similarity index 100% rename from apps/zui/src/models/editor-snapshot/compilation-error.ts rename to apps/zui/src/models/snapshot/compilation-error.ts diff --git a/apps/zui/src/models/editor-snapshot/source-set.ts b/apps/zui/src/models/snapshot/source-set.ts similarity index 100% rename from apps/zui/src/models/editor-snapshot/source-set.ts rename to apps/zui/src/models/snapshot/source-set.ts diff --git a/apps/zui/src/models/editor-snapshot/source.ts b/apps/zui/src/models/snapshot/source.ts similarity index 100% rename from apps/zui/src/models/editor-snapshot/source.ts rename to apps/zui/src/models/snapshot/source.ts diff --git a/apps/zui/src/models/editor-snapshot/types.ts b/apps/zui/src/models/snapshot/types.ts similarity index 100% rename from apps/zui/src/models/editor-snapshot/types.ts rename to apps/zui/src/models/snapshot/types.ts diff --git a/apps/zui/src/models/editor-snapshot/validator.ts b/apps/zui/src/models/snapshot/validator.ts similarity index 81% rename from apps/zui/src/models/editor-snapshot/validator.ts rename to apps/zui/src/models/snapshot/validator.ts index 3bf6d1df6b..ca11eac8d1 100644 --- a/apps/zui/src/models/editor-snapshot/validator.ts +++ b/apps/zui/src/models/snapshot/validator.ts @@ -1,14 +1,14 @@ import {Marker} from "src/js/state/Editor/types" -import {EditorSnapshot} from "../editor-snapshot" import {invoke} from "src/core/invoke" import {CompilationError} from "./compilation-error" import {DescribeErrorResponse} from "./types" +import {Snapshot} from "../snapshot" export class Validator { public errors: Marker[] = [] - async validate(snapshot: EditorSnapshot) { - const sourceSet = snapshot.toSourceSet() + async validate(snapshot: Snapshot) { + const sourceSet = snapshot.sourceSet const response: DescribeErrorResponse = await invoke( "editor.describe", sourceSet.contents diff --git a/apps/zui/src/modules/async-tasks/async-task.ts b/apps/zui/src/modules/async-tasks/async-task.ts index 041245b36d..4a36542a8d 100644 --- a/apps/zui/src/modules/async-tasks/async-task.ts +++ b/apps/zui/src/modules/async-tasks/async-task.ts @@ -10,7 +10,7 @@ export class AsyncTask { } abort() { - this.controller.abort() + this.controller.abort("Aborting task") } get signal() { diff --git a/apps/zui/src/modules/async-tasks/async-tasks.ts b/apps/zui/src/modules/async-tasks/async-tasks.ts index a814ba1b7b..d7f905509d 100644 --- a/apps/zui/src/modules/async-tasks/async-tasks.ts +++ b/apps/zui/src/modules/async-tasks/async-tasks.ts @@ -16,7 +16,7 @@ export class AsyncTasks { createOrReplace(tags: string[]) { this.abort(tags) - return this.create(tags) + return Promise.resolve(this.create(tags)) } abort(tags: string[]) { diff --git a/apps/zui/src/runners/application-runner.ts b/apps/zui/src/runners/application-runner.ts new file mode 100644 index 0000000000..c5e7c8566b --- /dev/null +++ b/apps/zui/src/runners/application-runner.ts @@ -0,0 +1,23 @@ +import {Dispatch, State, Store} from "src/js/state/types" + +type Selector = (state: State, ...args: any) => any + +export class ApplicationRunner { + static store: Store + + static select(selector: T): ReturnType { + return selector(this.store.getState()) + } + + static dispatch(action: Parameters[0]) { + return this.store.dispatch(action) + } + + protected dispatch(action: Parameters[0]) { + return ApplicationRunner.dispatch(action) + } + + protected select(selector: T): ReturnType { + return ApplicationRunner.select(selector) + } +} diff --git a/apps/zui/src/runners/queries-runner.ts b/apps/zui/src/runners/queries-runner.ts new file mode 100644 index 0000000000..7489610aee --- /dev/null +++ b/apps/zui/src/runners/queries-runner.ts @@ -0,0 +1,15 @@ +import {NamedQuery} from "src/models/named-query" +import {QuerySession} from "src/models/query-session" + +export class QueriesRunner { + open(id: string) { + const query = NamedQuery.find(id) + const session = QuerySession.activateOrCreate() + session.navigate({ + queryId: query.id, + sessionId: session.id, + pins: query.pins, + value: query.value, + }) + } +} diff --git a/apps/zui/src/test/unit/states/v1.18.0.json b/apps/zui/src/test/unit/states/v1.18.0.json new file mode 100644 index 0000000000..383b982108 --- /dev/null +++ b/apps/zui/src/test/unit/states/v1.18.0.json @@ -0,0 +1 @@ +{"version":202407221450,"data":{"order":["3eSnjxkG1QlCQm7-0CEcP"],"windows":{"3eSnjxkG1QlCQm7-0CEcP":{"name":"search","state":{"appearance":{"sidebarIsOpen":true,"sidebarWidth":250,"secondarySidebarIsOpen":true,"secondarySidebarWidth":400,"currentSectionName":"sessions","historyView":"linear","poolsOpenState":{},"queriesOpenState":{"4reK_WJ4v4UI0q6sl_hLc":true,"OMaQtdBskbxPb4eW2IWbS":true,"V3YJjtk1SYcAfuQ-rUOk9":true},"isSortingTabs":false},"tabHistories":{"ids":["SVgkxdlsrP7HMpLqv2A2b","5C78x3-BaMdyvz-zWVQaX","zp3s9k612NIhypmVQQoDM","6-TCKQhktLknobHclFpWJ","Ft5N0fW7NWAxaL4fNhJMg","DVHEVAMgAJyiGK8Hk1wA7","XHexNlmUveXI58EFBk61O"],"entities":{"SVgkxdlsrP7HMpLqv2A2b":{"id":"SVgkxdlsrP7HMpLqv2A2b","entries":["/welcome"],"index":0},"5C78x3-BaMdyvz-zWVQaX":{"id":"5C78x3-BaMdyvz-zWVQaX","entries":["/","/release-notes"],"index":1},"zp3s9k612NIhypmVQQoDM":{"id":"zp3s9k612NIhypmVQQoDM","entries":["/","/queries/zp3s9k612NIhypmVQQoDM/versions/0","/queries/zp3s9k612NIhypmVQQoDM/versions/r4b7zOLupw5hECILRq9gq","/queries/zp3s9k612NIhypmVQQoDM/versions/FPi8FmBhk6ddplo612U9S","/queries/zp3s9k612NIhypmVQQoDM/versions/wbWlffZaSUSgXDvmswnWk","/queries/zp3s9k612NIhypmVQQoDM/versions/2qNerzBr4JdeE8jvX15ia","/queries/yqECxyabSeUthCdIqQyqY/versions/Z1zit7_bWGlbM1aWrRHdQ","/queries/yqECxyabSeUthCdIqQyqY/versions/hCceRfGC8mi40aIjQHPH-","/queries/yqECxyabSeUthCdIqQyqY/versions/-HA88GICWVck59uO_TEu8","/queries/yqECxyabSeUthCdIqQyqY/versions/frEPiiTGdLJ842Ku41B_q","/queries/yqECxyabSeUthCdIqQyqY/versions/-PaUuzTq2L1tNjLa8ReEH","/queries/yqECxyabSeUthCdIqQyqY/versions/tuAJavVExjjHO5AdzZM13","/queries/yqECxyabSeUthCdIqQyqY/versions/1vg-BeIzyUZOOBeOt-yvn","/queries/yqECxyabSeUthCdIqQyqY/versions/_3I7koZQZjciogO_R6Vxx","/queries/yqECxyabSeUthCdIqQyqY/versions/H2oPLD8c8Qs9WTtQrotNs"],"index":14},"6-TCKQhktLknobHclFpWJ":{"id":"6-TCKQhktLknobHclFpWJ","entries":["/","/queries/6-TCKQhktLknobHclFpWJ/versions/0","/queries/6-TCKQhktLknobHclFpWJ/versions/fF0UkaUNGUZeNv-LWlnCw","/queries/6-TCKQhktLknobHclFpWJ/versions/ZpgSBKuHxx6ArbjfAs3cU","/queries/6-TCKQhktLknobHclFpWJ/versions/yv6I__GC86T6Cu_lmZE6U","/queries/6-TCKQhktLknobHclFpWJ/versions/6gR-hvBu4-IdRe1SuZo6h","/queries/6-TCKQhktLknobHclFpWJ/versions/AA4u27N9VcqTj012Yz0xr","/queries/6-TCKQhktLknobHclFpWJ/versions/0q3-V5Kvn_axMvKZdTm_H","/queries/6-TCKQhktLknobHclFpWJ/versions/TkisPzyDeDhHY08pPi_Cy","/queries/6-TCKQhktLknobHclFpWJ/versions/AIKP7oSAN0m7wvPiI1bOw","/queries/6-TCKQhktLknobHclFpWJ/versions/7JFye1iaeLt1_vaySmNMN","/queries/6-TCKQhktLknobHclFpWJ/versions/ylE4W52iWrZVkmtY_bMmP","/queries/6-TCKQhktLknobHclFpWJ/versions/AMBE5SAJbqt9S4LYl5Eex","/queries/6-TCKQhktLknobHclFpWJ/versions/bNj9nK9p0viKfPlb1r6eM","/queries/6-TCKQhktLknobHclFpWJ/versions/wa74e_GsndFFNFDErGZn0","/queries/6-TCKQhktLknobHclFpWJ/versions/tPGmsN8OW3rKWsi4LxIAi","/queries/6-TCKQhktLknobHclFpWJ/versions/8lRjR7X33LUJmztINyEBA","/queries/6-TCKQhktLknobHclFpWJ/versions/AfrOg0cwJg5TKjWgsLhl4","/queries/oUyb4EGXss7bLh_RHTRQx/versions/05E7pZWutSHCLx5Hm__X3","/queries/6-TCKQhktLknobHclFpWJ/versions/XHvtMJGs6BuV0zo2KZmEo","/queries/6-TCKQhktLknobHclFpWJ/versions/C45iznSvYNZUwWsH-igwK","/queries/j9eWeyfR0yLD_s3e9uyuw/versions/c1796y7HG6BO4fD42vABv","/queries/j9eWeyfR0yLD_s3e9uyuw/versions/wKJ7O5k7z63VKs3cFF6k-","/queries/ZUju9P8Ih02kuIZm_NFhk/versions/tNvXDtS8Mv9zq766cMqS6"],"index":23},"Ft5N0fW7NWAxaL4fNhJMg":{"id":"Ft5N0fW7NWAxaL4fNhJMg","entries":["/","/queries/Ft5N0fW7NWAxaL4fNhJMg/versions/0"],"index":1},"DVHEVAMgAJyiGK8Hk1wA7":{"id":"DVHEVAMgAJyiGK8Hk1wA7","entries":["/","/queries/DVHEVAMgAJyiGK8Hk1wA7/versions/0"],"index":1},"XHexNlmUveXI58EFBk61O":{"id":"XHexNlmUveXI58EFBk61O","entries":["/","/pools/0x136902a2a49be90b15f5be7dc0922931acbee818"],"index":1}}},"window":{"tabs":{"localhost:9867":{"active":"6-TCKQhktLknobHclFpWJ","preview":null,"data":[{"editor":{"value":"","pins":[],"pinEditIndex":null,"pinHoverIndex":null,"markers":[]},"id":"SVgkxdlsrP7HMpLqv2A2b","lastFocused":"2024-09-27T17:35:41.202Z","layout":{"columnHeadersView":"AUTO","resultsView":"TABLE","currentPaneName":"history","isEditingTitle":false,"titleFormAction":"create","showHistogram":true,"editorHeight":100,"chartHeight":100}},{"editor":{"value":"count() by _path | sort -r | count > 1000","pins":[{"type":"from","value":"Zeek"}],"pinEditIndex":null,"pinHoverIndex":null,"markers":[]},"id":"zp3s9k612NIhypmVQQoDM","lastFocused":"2024-09-27T17:50:00.000Z","layout":{"columnHeadersView":"AUTO","resultsView":"TABLE","currentPaneName":"history","isEditingTitle":false,"titleFormAction":"create","showHistogram":true,"editorHeight":100,"chartHeight":100}},{"editor":{"value":"","pins":[],"pinEditIndex":null,"pinHoverIndex":null,"markers":[]},"id":"Ft5N0fW7NWAxaL4fNhJMg","lastFocused":"2024-09-27T17:49:56.321Z","layout":{"columnHeadersView":"AUTO","resultsView":"TABLE","currentPaneName":"history","isEditingTitle":false,"titleFormAction":"create","showHistogram":true,"editorHeight":100,"chartHeight":100}},{"editor":{"value":"","pins":[],"pinEditIndex":null,"pinHoverIndex":null,"markers":[]},"id":"DVHEVAMgAJyiGK8Hk1wA7","lastFocused":"2024-09-27T17:49:57.799Z","layout":{"columnHeadersView":"AUTO","resultsView":"TABLE","currentPaneName":"history","isEditingTitle":false,"titleFormAction":"create","showHistogram":true,"editorHeight":100,"chartHeight":100}},{"editor":{"value":"","pins":[],"pinEditIndex":null,"pinHoverIndex":null,"markers":[]},"id":"XHexNlmUveXI58EFBk61O","lastFocused":"2024-09-27T17:49:58.564Z","layout":{"columnHeadersView":"AUTO","resultsView":"TABLE","currentPaneName":"history","isEditingTitle":false,"titleFormAction":"create","showHistogram":true,"editorHeight":100,"chartHeight":100}},{"editor":{"value":"sum(resp_bytes)","pins":[{"type":"from","value":"sample.zeektsv"}],"pinEditIndex":null,"pinHoverIndex":null,"markers":[]},"id":"6-TCKQhktLknobHclFpWJ","lastFocused":"2024-09-27T17:51:09.834Z","layout":{"columnHeadersView":"AUTO","resultsView":"TABLE","currentPaneName":"history","isEditingTitle":false,"titleFormAction":"create","showHistogram":true,"editorHeight":100,"chartHeight":100}}]}},"lakeId":"localhost:9867"}},"size":[1470,900],"position":[215,48]}},"globalState":{"configPropValues":{"application":{"updateMode":"startup"},"pools":{"nameDelimiter":"/"},"display":{"timeZone":"UTC","timeFormat":"","thousandsSeparator":",","decimal":"."},"editor":{"runQueryOnEnter":"enter"},"defaultLake":{"address":"localhost"},"brimcap":{"yamlConfigPath":"","suricataLocalRulesPath":"","pcapExtractionFolderPath":""}},"lakes":{"localhost:9867":{"host":"http://localhost","port":9867,"id":"localhost:9867","name":"jkerr's Zed Lake","authType":"none","version":"v1.18.0-4-gd1034203","features":{"describe":true}}},"launches":{"1.18.0":"2024-09-27T17:35:41.365Z"},"queries":{"id":"root","name":"root","isOpen":true,"items":[{"id":"yqECxyabSeUthCdIqQyqY","name":"Zeek Types with Over 1000 Logs"},{"name":"Folder A","id":"4reK_WJ4v4UI0q6sl_hLc","items":[{"name":"Folder A-1","id":"V3YJjtk1SYcAfuQ-rUOk9","items":[{"id":"ZUju9P8Ih02kuIZm_NFhk","name":"Sum Resp Bytes"}]},{"id":"oUyb4EGXss7bLh_RHTRQx","name":"Count of Cities"}]},{"name":"Folder B","id":"OMaQtdBskbxPb4eW2IWbS","items":[{"id":"j9eWeyfR0yLD_s3e9uyuw","name":"Sum Orig Bytes"}]}]},"queryVersions":{"zp3s9k612NIhypmVQQoDM":{"ids":["0","r4b7zOLupw5hECILRq9gq","FPi8FmBhk6ddplo612U9S","wbWlffZaSUSgXDvmswnWk","2qNerzBr4JdeE8jvX15ia","Z1zit7_bWGlbM1aWrRHdQ","hCceRfGC8mi40aIjQHPH-","-HA88GICWVck59uO_TEu8","frEPiiTGdLJ842Ku41B_q","-PaUuzTq2L1tNjLa8ReEH","tuAJavVExjjHO5AdzZM13","1vg-BeIzyUZOOBeOt-yvn","_3I7koZQZjciogO_R6Vxx","H2oPLD8c8Qs9WTtQrotNs"],"entities":{"0":{"ts":"2024-09-27T17:43:15.555Z","version":"0","value":"","pins":[]},"r4b7zOLupw5hECILRq9gq":{"version":"r4b7zOLupw5hECILRq9gq","ts":"2024-09-27T17:43:22.220Z","value":"","pins":[{"type":"from","value":"Zeek"}]},"FPi8FmBhk6ddplo612U9S":{"version":"FPi8FmBhk6ddplo612U9S","ts":"2024-09-27T17:43:29.058Z","value":"count() by _path","pins":[{"type":"from","value":"Zeek"}]},"wbWlffZaSUSgXDvmswnWk":{"version":"wbWlffZaSUSgXDvmswnWk","ts":"2024-09-27T17:43:33.647Z","value":"count() by _path | sort -r","pins":[{"type":"from","value":"Zeek"}]},"2qNerzBr4JdeE8jvX15ia":{"version":"2qNerzBr4JdeE8jvX15ia","ts":"2024-09-27T17:43:43.442Z","value":"count() by _path | sort -r | count > 500","pins":[{"type":"from","value":"Zeek"}]},"Z1zit7_bWGlbM1aWrRHdQ":{"version":"Z1zit7_bWGlbM1aWrRHdQ","ts":"2024-09-27T17:44:31.847Z","value":"count() by _path | sort -r | count > 500","pins":[{"type":"from","value":"Zeek"}]},"hCceRfGC8mi40aIjQHPH-":{"version":"hCceRfGC8mi40aIjQHPH-","ts":"2024-09-27T17:44:39.961Z","value":"count() by _path | sort -r | count > 200","pins":[{"type":"from","value":"Zeek"}]},"-HA88GICWVck59uO_TEu8":{"version":"-HA88GICWVck59uO_TEu8","ts":"2024-09-27T17:44:41.876Z","value":"count() by _path | sort -r | count > 200","pins":[{"type":"from","value":"Zeek"}]},"frEPiiTGdLJ842Ku41B_q":{"version":"frEPiiTGdLJ842Ku41B_q","ts":"2024-09-27T17:44:44.140Z","value":"count() by _path | sort -r | count > 100","pins":[{"type":"from","value":"Zeek"}]},"-PaUuzTq2L1tNjLa8ReEH":{"version":"-PaUuzTq2L1tNjLa8ReEH","ts":"2024-09-27T17:44:46.080Z","value":"count() by _path | sort -r | count > 100","pins":[{"type":"from","value":"Zeek"}]},"tuAJavVExjjHO5AdzZM13":{"version":"tuAJavVExjjHO5AdzZM13","ts":"2024-09-27T17:44:49.685Z","value":"count() by _path | sort -r | count > 500","pins":[{"type":"from","value":"Zeek"}]},"1vg-BeIzyUZOOBeOt-yvn":{"version":"1vg-BeIzyUZOOBeOt-yvn","ts":"2024-09-27T17:44:51.207Z","value":"count() by _path | sort -r | count > 500","pins":[{"type":"from","value":"Zeek"}]},"_3I7koZQZjciogO_R6Vxx":{"version":"_3I7koZQZjciogO_R6Vxx","ts":"2024-09-27T17:45:03.422Z","value":"count() by _path | sort -r | count > 1000","pins":[{"type":"from","value":"Zeek"}]},"H2oPLD8c8Qs9WTtQrotNs":{"version":"H2oPLD8c8Qs9WTtQrotNs","ts":"2024-09-27T17:45:04.594Z","value":"count() by _path | sort -r | count > 1000","pins":[{"type":"from","value":"Zeek"}]}}},"yqECxyabSeUthCdIqQyqY":{"ids":["Z1zit7_bWGlbM1aWrRHdQ","-HA88GICWVck59uO_TEu8","-PaUuzTq2L1tNjLa8ReEH","1vg-BeIzyUZOOBeOt-yvn","H2oPLD8c8Qs9WTtQrotNs"],"entities":{"Z1zit7_bWGlbM1aWrRHdQ":{"ts":"2024-09-27T17:44:31.847Z","version":"Z1zit7_bWGlbM1aWrRHdQ","value":"count() by _path | sort -r | count > 500","pins":[{"type":"from","value":"Zeek"}]},"-HA88GICWVck59uO_TEu8":{"version":"-HA88GICWVck59uO_TEu8","ts":"2024-09-27T17:44:41.876Z","value":"count() by _path | sort -r | count > 200","pins":[{"type":"from","value":"Zeek"}]},"-PaUuzTq2L1tNjLa8ReEH":{"version":"-PaUuzTq2L1tNjLa8ReEH","ts":"2024-09-27T17:44:46.080Z","value":"count() by _path | sort -r | count > 100","pins":[{"type":"from","value":"Zeek"}]},"1vg-BeIzyUZOOBeOt-yvn":{"version":"1vg-BeIzyUZOOBeOt-yvn","ts":"2024-09-27T17:44:51.207Z","value":"count() by _path | sort -r | count > 500","pins":[{"type":"from","value":"Zeek"}]},"H2oPLD8c8Qs9WTtQrotNs":{"version":"H2oPLD8c8Qs9WTtQrotNs","ts":"2024-09-27T17:45:04.594Z","value":"count() by _path | sort -r | count > 1000","pins":[{"type":"from","value":"Zeek"}]}}},"6-TCKQhktLknobHclFpWJ":{"ids":["0","fF0UkaUNGUZeNv-LWlnCw","ZpgSBKuHxx6ArbjfAs3cU","yv6I__GC86T6Cu_lmZE6U","6gR-hvBu4-IdRe1SuZo6h","AA4u27N9VcqTj012Yz0xr","0q3-V5Kvn_axMvKZdTm_H","TkisPzyDeDhHY08pPi_Cy","AIKP7oSAN0m7wvPiI1bOw","7JFye1iaeLt1_vaySmNMN","ylE4W52iWrZVkmtY_bMmP","AMBE5SAJbqt9S4LYl5Eex","bNj9nK9p0viKfPlb1r6eM","wa74e_GsndFFNFDErGZn0","tPGmsN8OW3rKWsi4LxIAi","8lRjR7X33LUJmztINyEBA","AfrOg0cwJg5TKjWgsLhl4","05E7pZWutSHCLx5Hm__X3","XHvtMJGs6BuV0zo2KZmEo","C45iznSvYNZUwWsH-igwK","c1796y7HG6BO4fD42vABv","wKJ7O5k7z63VKs3cFF6k-","tNvXDtS8Mv9zq766cMqS6"],"entities":{"0":{"ts":"2024-09-27T17:45:21.475Z","version":"0","value":"","pins":[]},"fF0UkaUNGUZeNv-LWlnCw":{"version":"fF0UkaUNGUZeNv-LWlnCw","ts":"2024-09-27T17:45:36.616Z","value":"","pins":[{"type":"from","value":"cities.json"}]},"ZpgSBKuHxx6ArbjfAs3cU":{"version":"ZpgSBKuHxx6ArbjfAs3cU","ts":"2024-09-27T17:45:42.357Z","value":"over children","pins":[{"type":"from","value":"cities.json"}]},"yv6I__GC86T6Cu_lmZE6U":{"version":"yv6I__GC86T6Cu_lmZE6U","ts":"2024-09-27T17:45:47.137Z","value":"over children | count() by id","pins":[{"type":"from","value":"cities.json"}]},"6gR-hvBu4-IdRe1SuZo6h":{"version":"6gR-hvBu4-IdRe1SuZo6h","ts":"2024-09-27T17:45:50.696Z","value":"over children","pins":[{"type":"from","value":"cities.json"}]},"AA4u27N9VcqTj012Yz0xr":{"version":"AA4u27N9VcqTj012Yz0xr","ts":"2024-09-27T17:46:07.968Z","value":"over this with children","pins":[{"type":"from","value":"cities.json"}]},"0q3-V5Kvn_axMvKZdTm_H":{"version":"0q3-V5Kvn_axMvKZdTm_H","ts":"2024-09-27T17:46:18.790Z","value":"","pins":[{"type":"from","value":"cities.json"}]},"TkisPzyDeDhHY08pPi_Cy":{"version":"TkisPzyDeDhHY08pPi_Cy","ts":"2024-09-27T17:46:27.589Z","value":"with this over children => this","pins":[{"type":"from","value":"cities.json"}]},"AIKP7oSAN0m7wvPiI1bOw":{"version":"AIKP7oSAN0m7wvPiI1bOw","ts":"2024-09-27T17:46:47.344Z","value":"over children with this => ","pins":[{"type":"from","value":"cities.json"}]},"7JFye1iaeLt1_vaySmNMN":{"version":"7JFye1iaeLt1_vaySmNMN","ts":"2024-09-27T17:46:50.780Z","value":"over children with this","pins":[{"type":"from","value":"cities.json"}]},"ylE4W52iWrZVkmtY_bMmP":{"version":"ylE4W52iWrZVkmtY_bMmP","ts":"2024-09-27T17:47:00.757Z","value":"over children with that:=this => (this)","pins":[{"type":"from","value":"cities.json"}]},"AMBE5SAJbqt9S4LYl5Eex":{"version":"AMBE5SAJbqt9S4LYl5Eex","ts":"2024-09-27T17:47:05.406Z","value":"over children with this => (this)","pins":[{"type":"from","value":"cities.json"}]},"bNj9nK9p0viKfPlb1r6eM":{"version":"bNj9nK9p0viKfPlb1r6eM","ts":"2024-09-27T17:47:24.485Z","value":"","pins":[{"type":"from","value":"cities.json"}]},"wa74e_GsndFFNFDErGZn0":{"version":"wa74e_GsndFFNFDErGZn0","ts":"2024-09-27T17:47:34.504Z","value":"over children","pins":[{"type":"from","value":"cities.json"}]},"tPGmsN8OW3rKWsi4LxIAi":{"version":"tPGmsN8OW3rKWsi4LxIAi","ts":"2024-09-27T17:47:38.526Z","value":"","pins":[{"type":"from","value":"cities.json"}]},"8lRjR7X33LUJmztINyEBA":{"version":"8lRjR7X33LUJmztINyEBA","ts":"2024-09-27T17:48:12.366Z","value":"over children","pins":[{"type":"from","value":"cities.json"}]},"AfrOg0cwJg5TKjWgsLhl4":{"version":"AfrOg0cwJg5TKjWgsLhl4","ts":"2024-09-27T17:48:16.266Z","value":"over children | count()","pins":[{"type":"from","value":"cities.json"}]},"05E7pZWutSHCLx5Hm__X3":{"version":"05E7pZWutSHCLx5Hm__X3","ts":"2024-09-27T17:48:22.688Z","value":"over children | count()","pins":[{"type":"from","value":"cities.json"}]},"XHvtMJGs6BuV0zo2KZmEo":{"version":"XHvtMJGs6BuV0zo2KZmEo","ts":"2024-09-27T17:48:38.826Z","value":"","pins":[{"type":"from","value":"sample.zeektsv"}]},"C45iznSvYNZUwWsH-igwK":{"version":"C45iznSvYNZUwWsH-igwK","ts":"2024-09-27T17:48:45.067Z","value":"sum(orig_bytes)","pins":[{"type":"from","value":"sample.zeektsv"}]},"c1796y7HG6BO4fD42vABv":{"version":"c1796y7HG6BO4fD42vABv","ts":"2024-09-27T17:48:52.522Z","value":"sum(orig_bytes)","pins":[{"type":"from","value":"sample.zeektsv"}]},"wKJ7O5k7z63VKs3cFF6k-":{"version":"wKJ7O5k7z63VKs3cFF6k-","ts":"2024-09-27T17:49:13.417Z","value":"sum(resp_bytes)","pins":[{"type":"from","value":"sample.zeektsv"}]},"tNvXDtS8Mv9zq766cMqS6":{"version":"tNvXDtS8Mv9zq766cMqS6","ts":"2024-09-27T17:49:17.224Z","value":"sum(resp_bytes)","pins":[{"type":"from","value":"sample.zeektsv"}]}}},"Ft5N0fW7NWAxaL4fNhJMg":{"ids":["0"],"entities":{"0":{"ts":"2024-09-27T17:47:50.691Z","version":"0","value":"","pins":[]}}},"DVHEVAMgAJyiGK8Hk1wA7":{"ids":["0"],"entities":{"0":{"ts":"2024-09-27T17:47:51.461Z","version":"0","value":"","pins":[]}}},"oUyb4EGXss7bLh_RHTRQx":{"ids":["05E7pZWutSHCLx5Hm__X3"],"entities":{"05E7pZWutSHCLx5Hm__X3":{"ts":"2024-09-27T17:48:22.688Z","version":"05E7pZWutSHCLx5Hm__X3","value":"over children | count()","pins":[{"type":"from","value":"cities.json"}]}}},"j9eWeyfR0yLD_s3e9uyuw":{"ids":["c1796y7HG6BO4fD42vABv"],"entities":{"c1796y7HG6BO4fD42vABv":{"ts":"2024-09-27T17:48:52.522Z","version":"c1796y7HG6BO4fD42vABv","value":"sum(orig_bytes)","pins":[{"type":"from","value":"sample.zeektsv"}]}}},"ZUju9P8Ih02kuIZm_NFhk":{"ids":["tNvXDtS8Mv9zq766cMqS6"],"entities":{"tNvXDtS8Mv9zq766cMqS6":{"ts":"2024-09-27T17:49:17.224Z","version":"tNvXDtS8Mv9zq766cMqS6","value":"sum(resp_bytes)","pins":[{"type":"from","value":"sample.zeektsv"}]}}}},"sessionQueries":{"SVgkxdlsrP7HMpLqv2A2b":{"id":"SVgkxdlsrP7HMpLqv2A2b","name":"Query Session"},"5C78x3-BaMdyvz-zWVQaX":{"id":"5C78x3-BaMdyvz-zWVQaX","name":"Query Session"},"7faDV4kxdTxw8QBvEbxLm":{"id":"7faDV4kxdTxw8QBvEbxLm","name":"Query Session"},"zp3s9k612NIhypmVQQoDM":{"id":"zp3s9k612NIhypmVQQoDM","name":"Query Session"},"6-TCKQhktLknobHclFpWJ":{"id":"6-TCKQhktLknobHclFpWJ","name":"Query Session"},"Ft5N0fW7NWAxaL4fNhJMg":{"id":"Ft5N0fW7NWAxaL4fNhJMg","name":"Query Session"},"DVHEVAMgAJyiGK8Hk1wA7":{"id":"DVHEVAMgAJyiGK8Hk1wA7","name":"Query Session"},"XHexNlmUveXI58EFBk61O":{"id":"XHexNlmUveXI58EFBk61O","name":"Query Session"},"XMgFORWG55yRl_ABawFuC":{"id":"XMgFORWG55yRl_ABawFuC","name":"Query Session"}},"poolSettings":{"ids":[],"entities":{}},"querySessions":{"ids":["zp3s9k612NIhypmVQQoDM","6-TCKQhktLknobHclFpWJ","Ft5N0fW7NWAxaL4fNhJMg","DVHEVAMgAJyiGK8Hk1wA7"],"entities":{"zp3s9k612NIhypmVQQoDM":{"id":"zp3s9k612NIhypmVQQoDM","createdAt":"2024-09-27T17:43:15.554Z","updatedAt":"2024-09-27T17:43:15.554Z"},"6-TCKQhktLknobHclFpWJ":{"id":"6-TCKQhktLknobHclFpWJ","createdAt":"2024-09-27T17:45:21.471Z","updatedAt":"2024-09-27T17:45:21.471Z"},"Ft5N0fW7NWAxaL4fNhJMg":{"id":"Ft5N0fW7NWAxaL4fNhJMg","createdAt":"2024-09-27T17:47:50.688Z","updatedAt":"2024-09-27T17:47:50.688Z"},"DVHEVAMgAJyiGK8Hk1wA7":{"id":"DVHEVAMgAJyiGK8Hk1wA7","createdAt":"2024-09-27T17:47:51.460Z","updatedAt":"2024-09-27T17:47:51.460Z"}}},"sessionHistories":{"zp3s9k612NIhypmVQQoDM":[{"queryId":"zp3s9k612NIhypmVQQoDM","version":"r4b7zOLupw5hECILRq9gq"},{"queryId":"zp3s9k612NIhypmVQQoDM","version":"FPi8FmBhk6ddplo612U9S"},{"queryId":"zp3s9k612NIhypmVQQoDM","version":"wbWlffZaSUSgXDvmswnWk"},{"queryId":"zp3s9k612NIhypmVQQoDM","version":"2qNerzBr4JdeE8jvX15ia"},{"queryId":"yqECxyabSeUthCdIqQyqY","version":"Z1zit7_bWGlbM1aWrRHdQ"},{"queryId":"yqECxyabSeUthCdIqQyqY","version":"hCceRfGC8mi40aIjQHPH-"},{"queryId":"yqECxyabSeUthCdIqQyqY","version":"-HA88GICWVck59uO_TEu8"},{"queryId":"yqECxyabSeUthCdIqQyqY","version":"frEPiiTGdLJ842Ku41B_q"},{"queryId":"yqECxyabSeUthCdIqQyqY","version":"-PaUuzTq2L1tNjLa8ReEH"},{"queryId":"yqECxyabSeUthCdIqQyqY","version":"tuAJavVExjjHO5AdzZM13"},{"queryId":"yqECxyabSeUthCdIqQyqY","version":"1vg-BeIzyUZOOBeOt-yvn"},{"queryId":"yqECxyabSeUthCdIqQyqY","version":"_3I7koZQZjciogO_R6Vxx"},{"queryId":"yqECxyabSeUthCdIqQyqY","version":"H2oPLD8c8Qs9WTtQrotNs"}],"6-TCKQhktLknobHclFpWJ":[{"queryId":"6-TCKQhktLknobHclFpWJ","version":"fF0UkaUNGUZeNv-LWlnCw"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"ZpgSBKuHxx6ArbjfAs3cU"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"yv6I__GC86T6Cu_lmZE6U"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"6gR-hvBu4-IdRe1SuZo6h"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"0q3-V5Kvn_axMvKZdTm_H"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"AMBE5SAJbqt9S4LYl5Eex"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"bNj9nK9p0viKfPlb1r6eM"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"wa74e_GsndFFNFDErGZn0"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"tPGmsN8OW3rKWsi4LxIAi"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"8lRjR7X33LUJmztINyEBA"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"AfrOg0cwJg5TKjWgsLhl4"},{"queryId":"oUyb4EGXss7bLh_RHTRQx","version":"05E7pZWutSHCLx5Hm__X3"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"XHvtMJGs6BuV0zo2KZmEo"},{"queryId":"6-TCKQhktLknobHclFpWJ","version":"C45iznSvYNZUwWsH-igwK"},{"queryId":"j9eWeyfR0yLD_s3e9uyuw","version":"c1796y7HG6BO4fD42vABv"},{"queryId":"j9eWeyfR0yLD_s3e9uyuw","version":"wKJ7O5k7z63VKs3cFF6k-"},{"queryId":"ZUju9P8Ih02kuIZm_NFhk","version":"tNvXDtS8Mv9zq766cMqS6"}]}}}} \ No newline at end of file diff --git a/apps/zui/src/views/application/index.tsx b/apps/zui/src/views/application/index.tsx index 203de4e3bb..6863b97e03 100644 --- a/apps/zui/src/views/application/index.tsx +++ b/apps/zui/src/views/application/index.tsx @@ -26,12 +26,12 @@ function AppRoutes() { - - - + + + diff --git a/apps/zui/src/views/application/use-shortcuts.ts b/apps/zui/src/views/application/use-shortcuts.ts index 1067f73c76..1b6478e1fc 100644 --- a/apps/zui/src/views/application/use-shortcuts.ts +++ b/apps/zui/src/views/application/use-shortcuts.ts @@ -5,6 +5,7 @@ import Modal from "../../js/state/Modal" import Tabs from "../../js/state/Tabs" import {useDispatch} from "src/core/use-dispatch" import cmd from "src/cmd" +import {QuerySession} from "src/models/query-session" export default function () { const dispatch = useDispatch() @@ -12,7 +13,7 @@ export default function () { const el = document.documentElement if (!el) throw new Error("No Document Element") const bindings = new Mousetrap(el) - .bind("mod+t", () => dispatch(Tabs.createQuerySession())) + .bind("mod+t", () => QuerySession.createAndActivate()) .bind("mod+w", (e) => { e.preventDefault() cmd.tabs.closeActive() diff --git a/apps/zui/src/views/histogram-pane/run-query.ts b/apps/zui/src/views/histogram-pane/run-query.ts index 7cd10788e5..b02d2083f1 100644 --- a/apps/zui/src/views/histogram-pane/run-query.ts +++ b/apps/zui/src/views/histogram-pane/run-query.ts @@ -16,11 +16,12 @@ export const runHistogramQuery = createHandler( const tabId = select(Current.getTabId) const taskId = "run-histogram-query-task" - asyncTasks.createOrReplace([tabId, taskId]).run(async (signal) => { + const task = await asyncTasks.createOrReplace([tabId, taskId]) + task.run(async (signal) => { await waitForSelector(QueryInfo.getIsParsed, {signal}).toReturn(true) const id = HISTOGRAM_RESULTS const key = select(Current.getLocation).key - const version = select(Current.getVersion) + const snapshot = select(Current.getSnapshot) const poolId = select(Current.getPoolFromQuery)?.id const baseQuery = select(Current.getQueryText) const {timeField, colorField} = select((s) => @@ -28,7 +29,7 @@ export const runHistogramQuery = createHandler( ) function getPinRange() { - const rangePin = version.pins.find( + const rangePin = snapshot.pins.find( (pin: QueryPin) => pin.type === "time-range" && !pin.disabled && diff --git a/apps/zui/src/views/pool-page/index.tsx b/apps/zui/src/views/pool-page/index.tsx index 8ddef02435..93ea19af5a 100644 --- a/apps/zui/src/views/pool-page/index.tsx +++ b/apps/zui/src/views/pool-page/index.tsx @@ -11,6 +11,7 @@ import {NotFound} from "./404" import {ButtonMenu} from "src/components/button-menu" import {RecentLoads} from "./recent-loads" import {Details} from "./details" +import {Active} from "src/models/active" const Toolbar = styled.div` display: flex; @@ -36,6 +37,10 @@ export function InitPool({children}) { if (poolId) dispatch(syncPool(poolId)) }, [poolId]) + useEffect(() => { + if (pool) Active.tab.setTitle(pool.name) + }, [pool]) + if (!pool) { return } else if (!pool.hasStats()) { diff --git a/apps/zui/src/views/release-notes/release-notes.tsx b/apps/zui/src/views/release-notes/release-notes.tsx index d6c5073e6d..28027a308f 100644 --- a/apps/zui/src/views/release-notes/release-notes.tsx +++ b/apps/zui/src/views/release-notes/release-notes.tsx @@ -1,8 +1,9 @@ import Markdown from "src/components/markdown" -import React from "react" +import React, {useLayoutEffect} from "react" import styled from "styled-components" import {useReleaseNotes} from "./use-release-notes" import {Content} from "src/js/components/Content" +import {Active} from "src/models/active" const Scrollable = styled.div` overflow: auto; @@ -15,6 +16,9 @@ const BG = styled(Content)` ` export default function ReleaseNotes() { + useLayoutEffect(() => { + Active.tab.setTitle("Release Notes") + }, []) const {notes, version, fetching} = useReleaseNotes() if (fetching) return null diff --git a/apps/zui/src/views/results-pane/run-results-query.tsx b/apps/zui/src/views/results-pane/run-results-query.tsx index 62a48b5b9a..610c9c1396 100644 --- a/apps/zui/src/views/results-pane/run-results-query.tsx +++ b/apps/zui/src/views/results-pane/run-results-query.tsx @@ -8,7 +8,7 @@ import {RESULTS_QUERY, RESULTS_QUERY_COUNT} from "./config" export const runResultsMain = createHandler( async ({select, dispatch, waitForSelector}) => { - const query = select(Current.getQueryText) + const query = Active.snapshot.queryText const tabId = select(Current.getTabId) dispatch(firstPage({id: RESULTS_QUERY, query})) diff --git a/apps/zui/src/views/right-pane/history/context-menu.ts b/apps/zui/src/views/right-pane/history/context-menu.ts new file mode 100644 index 0000000000..7cb32124da --- /dev/null +++ b/apps/zui/src/views/right-pane/history/context-menu.ts @@ -0,0 +1 @@ +export function contextMenu() {} diff --git a/apps/zui/src/views/right-pane/history/handler.ts b/apps/zui/src/views/right-pane/history/handler.ts new file mode 100644 index 0000000000..eac71795da --- /dev/null +++ b/apps/zui/src/views/right-pane/history/handler.ts @@ -0,0 +1,48 @@ +import {formatDistanceToNowStrict} from "date-fns" +import {useMemo} from "react" +import {useSelector} from "react-redux" +import {ViewHandler} from "src/core/view-handler" +import Current from "src/js/state/Current" +import Queries from "src/js/state/Queries" +import {QuerySession} from "src/models/query-session" +import {Snapshot} from "src/models/snapshot" + +export class HistoryHandler extends ViewHandler { + entries: Snapshot[] + + constructor() { + super() + this.entries = this.useEntries() + } + + useEntries() { + const sessionId = useSelector(Current.getSnapshot)?.sessionId + const snapshots = Snapshot.useWhere({sessionId}) + const _queries = useSelector(Queries.raw) + return useMemo( + () => + snapshots + .slice(0) + .reverse() + .filter((snapshot) => !snapshot.isEmpty), + [snapshots] + ) + } + + onActivate(id: string) { + const snapshot = Snapshot.find(id) + QuerySession.load(snapshot) + } + + formatTimestamp(date: Date) { + if (!date) return "-" + try { + let text = formatDistanceToNowStrict(date) + if (/second/.test(text)) return "now" + else return text.replace("second", "sec").replace("minute", "min") + } catch (e) { + console.error(e) + return "" + } + } +} diff --git a/apps/zui/src/views/right-pane/history/history-item.tsx b/apps/zui/src/views/right-pane/history/history-item.tsx index 62920d8709..f52f08142e 100644 --- a/apps/zui/src/views/right-pane/history/history-item.tsx +++ b/apps/zui/src/views/right-pane/history/history-item.tsx @@ -1,138 +1,32 @@ -import {formatDistanceToNowStrict} from "date-fns" -import React, {useMemo} from "react" -import {useSelector} from "react-redux" -import Current from "src/js/state/Current" -import Queries from "src/js/state/Queries" -import QueryVersions from "src/js/state/QueryVersions" -import styled from "styled-components" -import {useEntryMenu} from "./use-entry-menu" -import {State} from "src/js/state/types" -import {ActiveQuery} from "src/models/active-query" +import React from "react" import {NodeRendererProps} from "react-arborist" -import {Snapshots} from "src/domain/handlers" +import {Snapshot} from "src/models/snapshot" +import {HistoryHandler} from "./handler" +import classNames from "classnames" -const Wrap = styled.div` - height: 100%; - padding: 0 0.25rem; - cursor: default; -` -const BG = styled.div` - user-select: none; - height: 100%; - border-radius: 6px; - display: flex; - align-items: center; - padding: 0 1rem; - &:hover { - background-color: var(--emphasis-bg-less); - } - &:active { - background-color: var(--emphasis-bg-more); - } - &.deleted { - cursor: not-allowed; - } -` - -const Text = styled.p` - font-family: var(--mono-font); - font-size: var(--step--1); - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; - .anonymous &, - .modified &, - .deleted & { - opacity: 0.7; - font-weight: 400; - } -` - -const Timestamp = styled.p` - font-size: 10px; - opacity: 0.4; -` - -export type EntryType = - | "outdated" - | "latest" - | "anonymous" - | "deleted" - | "modified" - -type Props = { - version: string - queryId: string - index: number - id: string -} - -function getType(active: ActiveQuery): EntryType { - if (active.isDeleted()) return "deleted" - if (active.isLatest()) return "latest" - if (active.isOutdated()) return "outdated" - if (active.isModified()) return "modified" - return "anonymous" -} - -function getValue(active: ActiveQuery) { - if (active.isDeleted()) { - return "(Deleted)" - } else if (active.isAnonymous() || active.isModified()) { - return active.toZed() || "(Empty)" - } else { - return active.name() - } -} - -function getTimestamp(active: ActiveQuery) { - if (active.isDeleted() || !active.ts()) return "-" - const isoString = active.ts() - try { - let text = formatDistanceToNowStrict(new Date(isoString)) - if (/second/.test(text)) return "now" - else return text.replace("second", "sec").replace("minute", "min") - } catch (e) { - console.error(e) - return "" - } -} - -export function HistoryItem({node}: NodeRendererProps) { - const {index, queryId, version} = node.data - const onContextMenu = useEntryMenu(index) - const sessionId = useSelector(Current.getSessionId) - const session = useSelector(Current.getSession) - const build = useMemo(Queries.makeBuildSelector, []) - const query = useSelector((state: State) => build(state, queryId)) - const sVersion = useSelector((state) => - QueryVersions.at(sessionId).find(state, version) - ) - const qVersion = useSelector((state: State) => - QueryVersions.at(queryId).find(state, version) - ) - const versionObj = sVersion || qVersion - const active = new ActiveQuery(session, query, versionObj) - const onClick = () => { - if (active.isDeleted()) return - Snapshots.show({ - sessionId, - namedQueryId: queryId, - snapshotId: version, - }) - } - const type = getType(active) - const value = getValue(active) - const timestamp = getTimestamp(active) +type Props = NodeRendererProps & {handler: HistoryHandler} +export function HistoryItem({node, handler, style}: Props) { return ( - - - {value} - {timestamp} - - +
handler.onActivate(node.data.id)} + > +
+
+ + {node.data.query?.name ?? node.data.queryText} + + + {handler.formatTimestamp(node.data.createdAt)} + +
+
+
) } diff --git a/apps/zui/src/views/right-pane/history/section.tsx b/apps/zui/src/views/right-pane/history/section.tsx index f5f982105b..41dd76afdf 100644 --- a/apps/zui/src/views/right-pane/history/section.tsx +++ b/apps/zui/src/views/right-pane/history/section.tsx @@ -1,13 +1,12 @@ -import React, {useMemo} from "react" +import React from "react" import styled from "styled-components" -import Current from "src/js/state/Current" -import {useSelector} from "react-redux" import {HistoryItem} from "./history-item" import {isEmpty} from "lodash" import {EmptyText} from "src/components/empty-text" import {FillFlexParent} from "src/components/fill-flex-parent" import {Tree} from "react-arborist" import {TREE_ITEM_HEIGHT} from "../../sidebar/item" +import {HistoryHandler} from "./handler" const BG = styled.div` display: flex; @@ -17,16 +16,8 @@ const BG = styled.div` ` export function HistorySection() { - const sessionHistory = useSelector(Current.getSessionHistory) || [] - const history = useMemo( - () => - [...sessionHistory].reverse().map((d, index) => ({ - ...d, - id: (sessionHistory.length - 1 - index).toString(), - index: sessionHistory.length - 1 - index, - })), - [sessionHistory] - ) + const handler = new HistoryHandler() + const history = handler.entries return ( @@ -45,7 +36,7 @@ export function HistorySection() { disableDrag disableDrop > - {HistoryItem} + {(props) => } ) }} diff --git a/apps/zui/src/views/right-pane/history/use-entry-menu.ts b/apps/zui/src/views/right-pane/history/use-entry-menu.ts deleted file mode 100644 index 3b355a2917..0000000000 --- a/apps/zui/src/views/right-pane/history/use-entry-menu.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {useDispatch} from "react-redux" -import useSelect from "src/util/hooks/use-select" -import {showContextMenu} from "src/core/menu" -import Current from "src/js/state/Current" -import SessionHistories from "src/js/state/SessionHistories" - -export function useEntryMenu(index: number) { - const dispatch = useDispatch() - const select = useSelect() - - function onContextMenu() { - const sessionId = select(Current.getSessionId) - showContextMenu([ - { - label: "Remove", - click: () => { - dispatch(SessionHistories.deleteEntry({sessionId, index})) - }, - }, - ]) - } - - return onContextMenu -} diff --git a/apps/zui/src/views/right-pane/index.tsx b/apps/zui/src/views/right-pane/index.tsx index c4d0368673..23069c5a69 100644 --- a/apps/zui/src/views/right-pane/index.tsx +++ b/apps/zui/src/views/right-pane/index.tsx @@ -3,7 +3,6 @@ import React from "react" import {useSelector} from "react-redux" import Layout from "src/js/state/Layout" import {DraggablePane} from "src/js/components/draggable-pane" -import VersionsSection from "./versions-section" import AppErrorBoundary from "src/js/components/AppErrorBoundary" import {HistorySection} from "./history/section" import {ColumnsPane} from "src/views/columns-pane" @@ -18,8 +17,6 @@ function Contents() { switch (useSelector(Layout.getCurrentPaneName)) { case "detail": return - case "versions": - return case "history": return case "columns": diff --git a/apps/zui/src/views/right-pane/version-item.tsx b/apps/zui/src/views/right-pane/version-item.tsx deleted file mode 100644 index 44b35e237a..0000000000 --- a/apps/zui/src/views/right-pane/version-item.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, {useEffect, useState} from "react" -import styled from "styled-components" -import {formatDistanceToNowStrict} from "date-fns" -import {QueryVersion} from "src/js/state/QueryVersions/types" -import {NodeRendererProps} from "react-arborist" - -const TimeNode = styled.div` - display: flex; - align-items: center; -` -const Version = styled.div` - margin-right: 8px; - opacity: 0.5; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 8px; -` -const Dot = styled.div` - position: relative; - height: 100%; - height: 5px; - width: 5px; - margin: 0 16px; - - &:after { - content: ""; - background-color: var(--primary-color); - height: 5px; - width: 5px; - border-radius: 50%; - position: absolute; - } -` - -const Container = styled.div` - height: 100%; - width: 100%; - cursor: default; - user-select: none; - outline: none; - white-space: nowrap; - padding: 0 10px; - font-size: 15px; - - &:not(:last-child) { - ${Dot}:before { - content: ""; - position: absolute; - left: 2px; - top: 16px; - margin-top: 7px; - border-left: 1px solid var(--primary-color-dark); - height: 14px; - width: 1px; - } - } -` - -const BG = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 100%; - border-radius: 6px; - - &:hover { - background-color: rgba(0, 0, 0, 0.04); - } - &:active { - background-color: rgba(0, 0, 0, 0.06); - } - - &[aria-selected="true"] { - border-radius: 6px; - outline: none; - background-color: var(--primary-color); - color: white; - ${Dot}:after { - background-color: white; - } - &:hover { - background-color: var(--primary-color); - } - } -` - -const useForcedRenderInterval = (interval = 60000) => { - const [renderTrigger, setRenderTrigger] = useState(true) - useEffect(() => { - const id = setTimeout(() => setRenderTrigger(!renderTrigger), interval) - return () => clearTimeout(id) - }, [renderTrigger]) -} - -export const FormattedTime = ({ts}: {ts: string}) => { - useForcedRenderInterval() - - const duration = formatDistanceToNowStrict(new Date(ts)) - if (/second/.test(duration)) return Just now - return {duration} ago -} - -const VersionItem = ({ - node, - style, - dragHandle, -}: NodeRendererProps) => { - return ( - - - - - - - {node.data.value} - - - ) -} - -export default VersionItem diff --git a/apps/zui/src/views/right-pane/versions-section.test.tsx b/apps/zui/src/views/right-pane/versions-section.test.tsx deleted file mode 100644 index 4ece17a7a8..0000000000 --- a/apps/zui/src/views/right-pane/versions-section.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import React from "react" -import VersionsSection from "./versions-section" -import {queryPath} from "../../app/router/utils/paths" -import Queries from "src/js/state/Queries" -import QueryVersions from "src/js/state/QueryVersions" -import {QueryVersion} from "src/js/state/QueryVersions/types" -import {screen} from "src/test/unit/helpers" -import ResizeObserver from "resize-observer-polyfill" -import {SystemTest} from "src/test/system" - -global.ResizeObserver = ResizeObserver - -const system = new SystemTest("versions-section.test") - -const testQueryId = "testQueryId" -const testVersion1: QueryVersion = { - version: "v1", - ts: new Date(1).toISOString(), - value: "test value 1", - pins: [], -} -const testVersion2: QueryVersion = { - version: "v2 (latest)", - ts: new Date(2).toISOString(), - value: "test value 2", - pins: [], -} - -beforeEach(async () => { - system.store.dispatch(Queries.addItem({id: testQueryId, name: "test query"})) - system.store.dispatch(QueryVersions.at(testQueryId).create(testVersion1)) - system.store.dispatch(QueryVersions.at(testQueryId).create(testVersion2)) - system.navTo(queryPath(testQueryId, testVersion2.version)) - system.render() - await screen.findAllByText(/test value/i) -}) - -test("Display query version history in order", async () => { - const versions = screen.getAllByText(/test value/i) - expect(versions).toHaveLength(2) - expect(versions[0].textContent).toBe("test value 2") -}) diff --git a/apps/zui/src/views/right-pane/versions-section.tsx b/apps/zui/src/views/right-pane/versions-section.tsx deleted file mode 100644 index b1040cab7a..0000000000 --- a/apps/zui/src/views/right-pane/versions-section.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, {useMemo} from "react" -import VersionItem from "./version-item" -import {Tree} from "react-arborist" -import {useSelector} from "react-redux" -import Current from "src/js/state/Current" -import {QueryModel} from "src/js/models/query-model" -import {EmptyText} from "src/components/empty-text" -import {FillFlexParent} from "src/components/fill-flex-parent" -import {TREE_ITEM_HEIGHT} from "../sidebar/item" -import {NamedQueries} from "src/domain/handlers" - -const EmptyMessage = () => { - return Open a saved query to see the previous versions. -} - -const VersionsSection = () => { - const active = useSelector(Current.getActiveQuery) - if (active.isAnonymous()) { - return - } else { - return - } -} - -const VersionsList = ({query}: {query: QueryModel}) => { - const data = useMemo(() => { - return query.versions - .map((v) => ({...v, id: v.version})) - .sort((a, b) => (a.ts < b.ts ? 1 : -1)) - }, [query]) - - const currentId = useSelector(Current.getVersion)?.version ?? null - return ( - - {(dimens) => { - return ( - NamedQueries.show(query.id, node.id)} - > - {VersionItem} - - ) - }} - - ) -} - -export default VersionsSection diff --git a/apps/zui/src/views/session-page/editor-handler.ts b/apps/zui/src/views/session-page/editor-handler.ts index 3d2676cad5..2baca03483 100644 --- a/apps/zui/src/views/session-page/editor-handler.ts +++ b/apps/zui/src/views/session-page/editor-handler.ts @@ -5,6 +5,7 @@ import {submitSearch} from "src/domain/session/handlers" import Config from "src/js/state/Config" import Editor from "src/js/state/Editor" import {Active} from "src/models/active" +import {Snapshot} from "src/models/snapshot" export class EditorHandler extends ViewHandler { onChange(value) { @@ -26,7 +27,7 @@ export class EditorHandler extends ViewHandler { } private async validate() { - const {snapshot} = Active + const snapshot = new Snapshot(Active.editorState) if (await snapshot.isValid()) { this.dispatch(Editor.setMarkers([])) } else { diff --git a/apps/zui/src/views/session-page/handler.ts b/apps/zui/src/views/session-page/handler.ts index 7575940cef..36853e18ba 100644 --- a/apps/zui/src/views/session-page/handler.ts +++ b/apps/zui/src/views/session-page/handler.ts @@ -15,6 +15,9 @@ import {fetchQueryInfo} from "src/domain/session/handlers" import Current from "src/js/state/Current" import Pools from "src/js/state/Pools" import {syncPool} from "src/models/sync-pool" +import Table from "src/js/state/Table" +import Inspector from "src/js/state/Inspector" +import Selection from "src/js/state/Selection" type Props = { locationKey: string @@ -33,17 +36,20 @@ export class SessionPageHandler extends ViewHandler { } private reset() { + this.dispatch(Selection.reset()) + this.dispatch(Table.setScrollPosition({top: 0, left: 0})) + this.dispatch(Inspector.setScrollPosition({top: 0, left: 0})) this.dispatch(QueryInfo.reset()) this.dispatch(Tabs.loaded(this.props.locationKey)) this.dispatch(Notice.dismiss()) // This may not be needed any more } private setEditorValues() { - const snapshot = Active.session.snapshot + const snapshot = Active.snapshot // Give editor a chance to update by scheduling this update setTimeout(() => { - this.dispatch(Editor.setValue(snapshot.attrs.value ?? "")) - this.dispatch(Editor.setPins(snapshot.attrs.pins || [])) + this.dispatch(Editor.setValue(snapshot.value ?? "")) + this.dispatch(Editor.setPins(snapshot.pins || [])) }) } @@ -60,10 +66,8 @@ export class SessionPageHandler extends ViewHandler { } private async parseQueryText() { - const {session} = Active const lakeId = this.select(Current.getLakeId) const program = this.select(Current.getQueryText) - const history = this.select(Current.getHistory) if (!Active.lake.features.describe) { this.dispatch(QueryInfo.merge({isParsed: true})) @@ -81,8 +85,8 @@ export class SessionPageHandler extends ViewHandler { this.dispatch(syncPool(pool.id, lakeId)) } - if (!info.error && history.action === "PUSH") { - session.pushHistory() + if (info.error) { + // Maybe update the snapshot to indicate there is an error } }) } diff --git a/apps/zui/src/views/session-page/toolbar.tsx b/apps/zui/src/views/session-page/toolbar.tsx deleted file mode 100644 index 345ea5d676..0000000000 --- a/apps/zui/src/views/session-page/toolbar.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import {IconButton} from "src/components/icon-button" -import styles from "./toolbar.module.css" -import { - canGoBack, - canGoForward, - goBack, - goForward, -} from "src/domain/session/handlers/navigation" -import {useSelector} from "react-redux" -import Current from "src/js/state/Current" -import {editQuery} from "src/domain/session/handlers" -import Layout from "src/js/state/Layout" -import classNames from "classnames" -import {useTitleForm} from "./use-title-form" -import useSelect from "src/util/hooks/use-select" -import {ButtonMenu} from "src/components/button-menu" -import {useMenuExtension} from "src/core/menu" -import {createSelector} from "reselect" -import {sessionToolbarMenu} from "src/domain/session/menus/toolbar-menu" -import {useMemo} from "react" - -const getWhenContext = createSelector(Layout.getResultsView, (resultsView) => { - return { - "results.view": resultsView.toLowerCase(), - } -}) - -export function Toolbar() { - const context = useSelector(getWhenContext) - const query = useSelector(Current.getActiveQuery) - const defaultItems = useMemo(() => sessionToolbarMenu(query), [query]) - const items = useMenuExtension("results.toolbarMenu", defaultItems, context) - - return ( -
- - - -
- ) -} - -function QueryTitle() { - const query = useSelector(Current.getActiveQuery) - const isEditing = useSelector(Layout.getIsEditingTitle) - const form = useTitleForm() - const select = useSelect() - - function onBlur(e) { - if (select(Layout.getIsEditingTitle)) { - form.onSubmit(e) - } - } - - function onKeyUp(e) { - switch (e.key) { - case "Escape": - form.onReset() - break - case "Enter": - form.onSubmit(e) - break - } - } - - if (!isEditing) { - return ( - <> - - - ) - } else { - return ( -
- - -
- ) - } -} diff --git a/apps/zui/src/views/session-page/toolbar/handler.ts b/apps/zui/src/views/session-page/toolbar/handler.ts new file mode 100644 index 0000000000..bc8b78966b --- /dev/null +++ b/apps/zui/src/views/session-page/toolbar/handler.ts @@ -0,0 +1,149 @@ +import * as nav from "src/domain/session/handlers/navigation" +import {ViewHandler} from "src/core/view-handler" +import {useSelector} from "react-redux" +import {MenuItem, useMenuExtension} from "src/core/menu" +import * as get from "./selectors" +import {createMenu} from "./menu" +import {FormEvent} from "react" +import Current from "src/js/state/Current" +import {Snapshot} from "src/models/snapshot" +import {useZuiApi} from "src/views/application/context" +import ZuiApi from "src/js/api/zui-api" +import Layout from "src/js/state/Layout" +import {editQuery} from "src/domain/session/handlers" +import {Active} from "src/models/active" +import {plusOne} from "src/util/plus-one" +import {NamedQuery} from "src/models/named-query" +import Queries from "src/js/state/Queries" +import {Query} from "src/js/state/Queries/types" + +export class ToolbarHandler extends ViewHandler { + nav = nav + menuItems: MenuItem[] + isEditing: boolean + snapshot: Snapshot + oldApi: ZuiApi + isSubmitting = false + isModified: boolean + query: Query | null + + constructor() { + super() + this.oldApi = useZuiApi() + this.snapshot = useSelector(Current.getSnapshot) + this.query = useSelector(Current.getQuery) + this.isModified = useSelector(Current.getQueryIsModified) + this.isEditing = useSelector(Layout.getIsEditingTitle) + this.menuItems = useMenuExtension( + "results.toolbarMenu", + createMenu(), + useSelector(get.whenContext) + ) + this.listen({ + "session.resetQuery": () => this.onDetach(), + "session.saveAsNewQuery": () => this.onSaveAs(), + "session.updateQuery": () => this.onUpdate(), + }) + } + + onSubmit(e: FormEvent) { + if (this.isSubmitting) return + this.isSubmitting = true + e.preventDefault() + const input = e.currentTarget.elements.namedItem("query-name") as any + const name = input.value.trim() || "" + if (name.length) { + this.hasQuery ? this.onRename(name) : this.onCreate(name) + } + this.hideForm() + } + + onCreate(name: string) { + const {pins, value} = Active.editorState + const session = Active.querySession + const query = NamedQuery.create({name, value, pins}) + const snapshot = Snapshot.create({ + queryId: query.id, + sessionId: session.id, + pins, + value, + }) + session.tab.load(snapshot.pathname) + } + + onRename(name: string) { + const query = NamedQuery.find(this.queryId) + query.update({name}) + } + + onSaveAs() { + const name = this.hasQuery ? this.queryName : "" + const newName = plusOne(name) + this.onCreate(newName) + setTimeout(() => { + this.dispatch(Layout.showTitleForm()) + }) + } + + onUpdate() { + this.dispatch( + Queries.editItem({ + id: this.queryId, + changes: Active.editorState, + }) + ) + } + + onReset() { + this.hideForm() + } + + onDetach() { + const session = Active.querySession + const next = Snapshot.create({ + sessionId: session.id, + queryId: null, + ...Active.editorState, + }) + session.tab.load(next.pathname) + } + + onBlur(e: FormEvent) { + if (this.isEditing) this.onSubmit(e) + } + + onKeyUp(e: any) { + switch (e.key) { + case "Escape": + this.onReset() + break + case "Enter": + this.onSubmit(e) + break + } + } + + onEdit() { + editQuery() + } + + get hasQuery() { + return !!this.query + } + + get queryName() { + if (this.hasQuery) { + return this.query.name + } else { + return undefined + } + } + + get queryId() { + return this.snapshot.queryId + } + + private hideForm() { + this.dispatch(Layout.hideTitleForm()) + } +} diff --git a/apps/zui/src/views/session-page/toolbar/index.tsx b/apps/zui/src/views/session-page/toolbar/index.tsx new file mode 100644 index 0000000000..bf11521789 --- /dev/null +++ b/apps/zui/src/views/session-page/toolbar/index.tsx @@ -0,0 +1,32 @@ +import {IconButton} from "src/components/icon-button" +import styles from "../toolbar.module.css" +import {ButtonMenu} from "src/components/button-menu" +import {Title} from "./title" +import {ToolbarHandler} from "./handler" + +export function Toolbar() { + const handler = new ToolbarHandler() + + return ( +
+ + + <ButtonMenu items={handler.menuItems} label={"Results Toolbar Menu"} /> + </div> + ) +} diff --git a/apps/zui/src/domain/session/menus/toolbar-menu.ts b/apps/zui/src/views/session-page/toolbar/menu.ts similarity index 79% rename from apps/zui/src/domain/session/menus/toolbar-menu.ts rename to apps/zui/src/views/session-page/toolbar/menu.ts index e5e4b8cef0..576c31a0fe 100644 --- a/apps/zui/src/domain/session/menus/toolbar-menu.ts +++ b/apps/zui/src/views/session-page/toolbar/menu.ts @@ -1,19 +1,18 @@ -import {createMenu} from "src/core/menu" -import {ActiveQuery} from "src/models/active-query" +import {MenuItem} from "src/core/menu" -export const sessionToolbarMenu = createMenu((_, query: ActiveQuery) => { +export function createMenu() { return [ { label: "Update Query", - command: "namedQueries.update", + command: "session.updateQuery", iconName: "check", - visible: query.isModified(), + when: "session.hasModifiedQuery", }, { label: "Detach from Query", command: "session.resetQuery", iconName: "close_circle", - visible: query.isSaved(), + when: "session.hasQuery", }, { label: "Save as New Query", @@ -54,5 +53,5 @@ export const sessionToolbarMenu = createMenu((_, query: ActiveQuery) => { iconName: "run", command: "session.runQuery", }, - ] -}) + ] as MenuItem[] +} diff --git a/apps/zui/src/views/session-page/toolbar/selectors.ts b/apps/zui/src/views/session-page/toolbar/selectors.ts new file mode 100644 index 0000000000..b14cf7e20c --- /dev/null +++ b/apps/zui/src/views/session-page/toolbar/selectors.ts @@ -0,0 +1,16 @@ +import {createSelector} from "@reduxjs/toolkit" +import Current from "src/js/state/Current" +import Layout from "src/js/state/Layout" + +export const whenContext = createSelector( + Layout.getResultsView, + Current.getQuery, + Current.getQueryIsModified, + (resultsView, query, isModified) => { + return { + "results.view": resultsView.toLowerCase(), + "session.hasQuery": !!query, + "session.hasModifiedQuery": isModified, + } + } +) diff --git a/apps/zui/src/views/session-page/toolbar/title.tsx b/apps/zui/src/views/session-page/toolbar/title.tsx new file mode 100644 index 0000000000..dee0f933a7 --- /dev/null +++ b/apps/zui/src/views/session-page/toolbar/title.tsx @@ -0,0 +1,45 @@ +import {ToolbarHandler} from "./handler" +import styles from "../toolbar.module.css" +import classNames from "classnames" + +export function Title({handler}: {handler: ToolbarHandler}) { + if (!handler.isEditing) { + return ( + <> + <button className={styles.button}> + <h1 + onClick={() => handler.onEdit()} + className={classNames({[styles.modified]: handler.isModified})} + > + {handler.hasQuery ? ( + handler.queryName + ) : ( + <span className={styles.untitled}>Untitled</span> + )} + {handler.isModified && "*"} + </h1> + </button> + </> + ) + } else { + return ( + <form + className={classNames(styles.form)} + onBlur={(e) => handler.onBlur(e)} + onKeyUp={(e) => handler.onKeyUp(e)} + > + <label htmlFor="query-name" style={{display: "none"}}> + Query Name + </label> + <input + id="query-name" + name="query-name" + placeholder="Name your query..." + autoFocus + className={styles.input} + defaultValue={handler.queryName} + /> + </form> + ) + } +} diff --git a/apps/zui/src/views/session-page/use-title-form.ts b/apps/zui/src/views/session-page/use-title-form.ts deleted file mode 100644 index 67e98c687b..0000000000 --- a/apps/zui/src/views/session-page/use-title-form.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {FormEvent} from "react" -import {useSelector} from "react-redux" -import {useZuiApi} from "src/views/application/context" -import {useDispatch} from "src/core/use-dispatch" -import Layout from "src/js/state/Layout" -import Current from "src/js/state/Current" -import {create} from "src/domain/named-queries/handlers" - -export function useTitleForm() { - const active = useSelector(Current.getActiveQuery) - const api = useZuiApi() - const dispatch = useDispatch() - - return { - onSubmit: (e: FormEvent<HTMLFormElement>) => { - e.preventDefault() - const input = e.currentTarget.elements.namedItem("query-name") as any - const name = input.value.trim() || "" - - if (name.length) { - if (active.isSaved() && !active.isModified()) { - api.queries.rename(active.query.id, name) - } else { - create(name) - } - } - dispatch(Layout.hideTitleForm()) - }, - onReset: () => dispatch(Layout.hideTitleForm()), - } -} diff --git a/apps/zui/src/views/sessions-pane/index.tsx b/apps/zui/src/views/sessions-pane/index.tsx index d71f9b55f7..2a4de81269 100644 --- a/apps/zui/src/views/sessions-pane/index.tsx +++ b/apps/zui/src/views/sessions-pane/index.tsx @@ -1,13 +1,11 @@ import {useSelector} from "react-redux" import {Icon} from "src/components/icon" import {VirtualList} from "src/js/components/virtual-list" -import SessionHistories from "src/js/state/SessionHistories" import {QuerySession} from "src/models/query-session" import {SessionsPaneHandler} from "./handler" import Tabs from "src/js/state/Tabs" export function SessionsPane() { - useSelector(SessionHistories.raw) // We need this here to update the display name useSelector(Tabs.getActive) // We need this to update isActive const sessions = QuerySession.useAll().sort( (item, pivot) => pivot.createdAt.getTime() - item.createdAt.getTime() diff --git a/apps/zui/src/views/sidebar/plus-button.tsx b/apps/zui/src/views/sidebar/plus-button.tsx index 89d6bb9a29..8f48965b09 100644 --- a/apps/zui/src/views/sidebar/plus-button.tsx +++ b/apps/zui/src/views/sidebar/plus-button.tsx @@ -6,10 +6,10 @@ import {useZuiApi} from "src/views/application/context" import {MenuItem, showContextMenu} from "src/core/menu" import {useDispatch} from "src/core/use-dispatch" import useLakeId from "src/app/router/hooks/use-lake-id" -import Tabs from "src/js/state/Tabs" import {Icon} from "src/components/icon" import {connectToLake} from "src/app/commands/connect-to-lake" import Modal from "src/js/state/Modal" +import {QuerySession} from "src/models/query-session" export const Button = styled.button` color: white; @@ -50,7 +50,7 @@ export default function PlusButton() { const template: MenuItem[] = [ { label: "New Query Session", - click: () => dispatch(Tabs.createQuerySession()), + click: () => QuerySession.createAndActivate(), }, { label: "New Pool", diff --git a/apps/zui/src/views/sidebar/queries-section/queries-tree.tsx b/apps/zui/src/views/sidebar/queries-section/queries-tree.tsx index ba6aa99763..abb787da47 100644 --- a/apps/zui/src/views/sidebar/queries-section/queries-tree.tsx +++ b/apps/zui/src/views/sidebar/queries-section/queries-tree.tsx @@ -17,7 +17,8 @@ import Appearance from "src/js/state/Appearance" import {TREE_ITEM_HEIGHT} from "../item" import {showMenu} from "src/core/menu" import EmptySection from "src/components/empty-section" -import {NamedQueries} from "src/domain/handlers" +import {NamedQuery} from "src/models/named-query" +import {QueriesRunner} from "src/runners/queries-runner" type Props = { searchTerm: string @@ -40,7 +41,8 @@ function TreeOfQueries(props: { }) { const dispatch = useDispatch() const api = useZuiApi() - const id = useSelector(Current.getSessionRouteParentId) + const snapshot = useSelector(Current.getSnapshot) + const id = snapshot?.queryId const tree = useRef<TreeApi<Query | Group>>() const [{isOver}, drop] = useQueryImportOnDrop() const initialOpenState = useSelector(Appearance.getQueriesOpenState) @@ -74,7 +76,7 @@ function TreeOfQueries(props: { childrenAccessor="items" onActivate={(node) => { if (node.isLeaf && id !== node.id) { - NamedQueries.show(node.id) + new QueriesRunner().open(node.id) } }} onMove={(args) => { @@ -83,16 +85,15 @@ function TreeOfQueries(props: { ) }} onRename={async (args) => { - await api.queries.update({ - id: args.id, - changes: {name: args.name}, - }) + NamedQuery.find(args.id).update({name: args.name}) }} onCreate={({type, parentId}) => { if (type === "leaf") { - return api.queries.create({ + return NamedQuery.create({ name: "", parentId, + value: "", + pins: [], }) } else { return api.queries.createGroup("", parentId) diff --git a/apps/zui/src/views/sidebar/sidebar-toggle-button.tsx b/apps/zui/src/views/sidebar/sidebar-toggle-button.tsx index deade549ed..23fe19257a 100644 --- a/apps/zui/src/views/sidebar/sidebar-toggle-button.tsx +++ b/apps/zui/src/views/sidebar/sidebar-toggle-button.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, {CSSProperties} from "react" import {useDispatch} from "src/core/use-dispatch" import Appearance from "src/js/state/Appearance" import {IconButton} from "src/components/icon-button" @@ -19,12 +19,13 @@ export const SidebarToggleButton = () => { ) } -export const RightSidebarToggleButton = () => { +export const RightSidebarToggleButton = (props: {style?: CSSProperties}) => { const dispatch = useDispatch() const open = useSelector(Appearance.secondarySidebarIsOpen) const name = open ? "layout_rightbar_close" : "layout_rightbar_open" return ( <IconButton + style={props.style} iconName={name} label="Toggle Right Sidebar" data-tooltip="Toggle Right Sidebar" diff --git a/apps/zui/src/views/tab-bar/handler.ts b/apps/zui/src/views/tab-bar/handler.ts index 12f58616d9..daf0e76605 100644 --- a/apps/zui/src/views/tab-bar/handler.ts +++ b/apps/zui/src/views/tab-bar/handler.ts @@ -3,10 +3,8 @@ import {MutableRefObject, useRef} from "react" import {useSelector} from "react-redux" import {StateObject, useStateObject} from "src/core/state-object" import {ViewHandler} from "src/core/view-handler" -import tab from "src/js/models/tab" import Appearance from "src/js/state/Appearance" import Tabs from "src/js/state/Tabs" -import {getTabModels} from "src/js/state/Tabs/get-tab-models" import { SortableList, SortableListArgs, @@ -14,6 +12,8 @@ import { import {getGap, getRect} from "./utils" import {move} from "src/modules/sortable-list-algorithm/utils" import {BrowserTab} from "src/models/browser-tab" +import {getTabDisplayProps} from "src/js/state/Tabs/get-display-props" +import {QuerySession} from "src/models/query-session" type XY = {x: number; y: number} export const initialState = { @@ -21,7 +21,7 @@ export const initialState = { } export class TabBarHandler extends ViewHandler { - tabs: ReturnType<typeof tab>[] + tabs: {id: string; title: string}[] activeId: string previewId: string sidebarOpen: boolean @@ -37,7 +37,7 @@ export class TabBarHandler extends ViewHandler { this.previewId = useSelector(Tabs.getPreview) this.sidebarOpen = useSelector(Appearance.sidebarIsOpen) this.secondarySidebarOpen = useSelector(Appearance.secondarySidebarIsOpen) - this.tabs = useSelector(getTabModels) + this.tabs = useSelector(getTabDisplayProps) this.sortableState = useStateObject(SortableList.initialState()) this.sortableList = new SortableList(this.sortableState) this.listRef = useRef<HTMLElement>() @@ -57,7 +57,7 @@ export class TabBarHandler extends ViewHandler { } create() { - this.dispatch(Tabs.createQuerySession()) + QuerySession.createAndActivate() } destroy(e: any, id: string) { @@ -162,7 +162,7 @@ export class TabBarHandler extends ViewHandler { if (this.state.isDropping) { return { ...this.sortableList.previewDimens, - x: this.sortableList.dstItem.startPoint, + x: (this.sortableList.dstItem || this.sortableList.srcItem)?.startPoint, } } else { return this.sortableList.previewDimens diff --git a/apps/zui/src/views/tab-bar/index.tsx b/apps/zui/src/views/tab-bar/index.tsx index 43e71fcf9d..ae5c4d6b69 100644 --- a/apps/zui/src/views/tab-bar/index.tsx +++ b/apps/zui/src/views/tab-bar/index.tsx @@ -36,7 +36,9 @@ export function TabBar() { click={() => handler.create()} label="New Tab" /> - {handler.showSecondarySidebarToggle && <RightSidebarToggleButton />} + {handler.showSecondarySidebarToggle && ( + <RightSidebarToggleButton style={{marginInlineStart: "auto"}} /> + )} </div> ) } diff --git a/apps/zui/src/views/tab-bar/tab-item-drag-preview.tsx b/apps/zui/src/views/tab-bar/tab-item-drag-preview.tsx index 556b773ddf..bfbdd6ecd2 100644 --- a/apps/zui/src/views/tab-bar/tab-item-drag-preview.tsx +++ b/apps/zui/src/views/tab-bar/tab-item-drag-preview.tsx @@ -1,6 +1,7 @@ import {Icon} from "src/components/icon" import {IconButton} from "src/components/icon-button" import {TabBarHandler} from "./handler" +import {BrowserTab} from "src/models/browser-tab" type Props = { handler: TabBarHandler @@ -9,14 +10,15 @@ type Props = { export function TabItemDragPreview({handler}: Props) { const tab = handler.srcTab const {x, y, width, height} = handler.dragPreviewDimens + const browserTab = BrowserTab.find(tab.id) return ( <div className={handler.dragPreviewClassNames} style={{transform: `translate(${x}px, ${y}px)`, width, height}} > <span className="tab-item-title"> - <Icon name={tab.icon()} className="tab-icon" /> - {tab.title()} + <Icon name={browserTab.iconName} className="tab-icon" /> + {tab.title} </span> <IconButton className="tab-item-close-button" diff --git a/apps/zui/src/views/tab-bar/tab-item.tsx b/apps/zui/src/views/tab-bar/tab-item.tsx index f1ce1f0db0..97a6e8583a 100644 --- a/apps/zui/src/views/tab-bar/tab-item.tsx +++ b/apps/zui/src/views/tab-bar/tab-item.tsx @@ -2,11 +2,11 @@ import {Icon} from "src/components/icon" import {IconButton} from "src/components/icon-button" import {useDrag} from "@react-aria/dnd" import {useRef} from "react" -import tab from "src/js/models/tab" import {TabBarHandler} from "./handler" +import {BrowserTab} from "src/models/browser-tab" type Props = { - tab: ReturnType<typeof tab> + tab: {id: string; title: string} handler: TabBarHandler index: number } @@ -20,6 +20,7 @@ export function TabItem({tab, handler, index}: Props) { onDragMove: (e) => handler.onDragMove(e), onDragEnd: () => handler.onDragEnd(), }) + const browserTab = BrowserTab.find(tab.id) return ( <div @@ -31,8 +32,8 @@ export function TabItem({tab, handler, index}: Props) { onMouseDown={() => handler.activate(tab.id)} > <span className="tab-item-title"> - <Icon name={tab.icon()} className="tab-icon" /> - {tab.title()} + <Icon name={browserTab.iconName} className="tab-icon" /> + {tab.title} </span> <IconButton className="tab-item-close-button" diff --git a/apps/zui/src/views/welcome-page/index.tsx b/apps/zui/src/views/welcome-page/index.tsx index 213a1ce926..98ed689eb4 100644 --- a/apps/zui/src/views/welcome-page/index.tsx +++ b/apps/zui/src/views/welcome-page/index.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, {useLayoutEffect} from "react" import {connectToLake} from "src/app/commands/connect-to-lake" import {Subtitle} from "src/components/subtitle" import {Title} from "src/components/title" @@ -6,6 +6,7 @@ import styled from "styled-components" import links from "src/config/links" import {invoke} from "src/core/invoke" import {chooseFiles} from "src/domain/loads/handlers" +import {Active} from "src/models/active" const BG = styled.div` background-image: url(/welcome-page-background.svg); @@ -43,6 +44,10 @@ const Actions = styled.section` ` export function WelcomePage() { + useLayoutEffect(() => { + Active.tab.setTitle("Welcome") + }, []) + return ( <BG> <Title> diff --git a/packages/zed-js/src/client/utils.ts b/packages/zed-js/src/client/utils.ts index 786cf67c86..b4896bb2a5 100644 --- a/packages/zed-js/src/client/utils.ts +++ b/packages/zed-js/src/client/utils.ts @@ -75,7 +75,7 @@ export function jsonHeader(obj: object) { export function wrapAbort(signal?: IsoAbortSignal) { const ctl = new AbortController(); - signal?.addEventListener('abort', () => ctl.abort()); + signal?.addEventListener('abort', () => ctl.abort(signal.reason)); return ctl; } diff --git a/packages/zui-player/helpers/test-app.ts b/packages/zui-player/helpers/test-app.ts index f04cac58cf..83dfb7fdc9 100644 --- a/packages/zui-player/helpers/test-app.ts +++ b/packages/zui-player/helpers/test-app.ts @@ -42,7 +42,6 @@ export default class TestApp { if (process.env.VIDEO == 'true') { launchOpts.recordVideo = { dir: path.join('run', 'videos') }; } - // @ts-ignore if (bin) launchOpts.executablePath = bin; this.zui = await electron.launch(launchOpts); @@ -51,7 +50,6 @@ export default class TestApp { if (process.env['VERBOSE']) { this.zui.process().stdout.pipe(process.stdout); } - await waitForTrue(() => this.zui.windows().length === 2); await waitForTrue(async () => !!(await this.getWindowByTitle('Zui'))); await waitForTrue( diff --git a/packages/zui-player/tests/title-bar-buttons.spec.ts b/packages/zui-player/tests/title-bar-buttons.spec.ts index 6ffe96d5a6..12b346fbdc 100644 --- a/packages/zui-player/tests/title-bar-buttons.spec.ts +++ b/packages/zui-player/tests/title-bar-buttons.spec.ts @@ -14,9 +14,10 @@ play('title bar buttons', (app, test) => { }); test('toggle right sidebar', async () => { + await app.attached('button', 'History'); await app.click('button', 'Toggle Right Sidebar'); - await app.detached('button', 'HISTORY'); + await app.detached('button', 'History'); await app.click('button', 'Toggle Right Sidebar'); - await app.attached(/Session history will appear here/); + await app.attached('button', 'History'); }); }); diff --git a/packages/zui-test-data/data/brimcap-queries.json b/packages/zui-test-data/data/brimcap-queries.json index 95f31ad170..f607bc5094 100644 --- a/packages/zui-test-data/data/brimcap-queries.json +++ b/packages/zui-test-data/data/brimcap-queries.json @@ -1 +1,65 @@ -{ "name": "Brimcap", "items": [ { "name": "Activity Overview", "value": "count() by _path | sort -r", "description": "Shows a list of all Zeek streams in the data set, with a count of associated records" }, { "name": "Unique DNS Queries", "value": "_path==\"dns\" | count() by query | sort -r", "description": "Shows all unique DNS queries in the data set with count" }, { "name": "Windows Networking Activity", "value": "grep(smb*,_path) OR _path==\"dce_rpc\"", "description": "Filters and displays smb_files, smb_mapping and DCE_RPC activity" }, { "name": "HTTP Requests", "value": "_path==\"http\" | cut id.orig_h, id.resp_h, id.resp_p, method, host, uri | uniq -c", "description": "Displays a list of the count of unique HTTP requests including source and destination" }, { "name": "Unique Network Connections", "value": "_path==\"conn\" | cut id.orig_h, id.resp_p, id.resp_h | sort | uniq", "description": "Displays a table showing all unique source:port:destination connections pairings" }, { "name": "Connection Received Data", "value": "_path==\"conn\" | put total_bytes := orig_bytes + resp_bytes | sort -r total_bytes | cut uid, id, orig_bytes, resp_bytes, total_bytes", "description": "Shows the connections between hosts, sorted by data received" }, { "name": "File Activity", "value": "filename!=null | cut _path, tx_hosts, rx_hosts, conn_uids, mime_type, filename, md5, sha1", "description": "Displays a curated view of file data including md5 and sha1 for complete file transfers" }, { "name": "HTTP Post Requests", "value": "method==\"POST\" | cut ts, uid, id, method, uri, status_code", "description": "Displays all HTTP Post requests including the URI and HTTP status code" }, { "name": "Show IP Subnets", "value": "_path==\"conn\" | put classnet := network_of(id.resp_h) | cut classnet | count() by classnet | sort -r", "description": "Enumerates the classful networks for all destination IP addresses including count of connections" }, { "name": "Suricata Alerts by Category", "value": "event_type==\"alert\" | count() by alert.severity,alert.category | sort count", "description": "Shows all Suricata alert counts by category and severity" }, { "name": "Suricata Alerts by Source and Destination", "value": "event_type==\"alert\" | alerts := union(alert.category) by src_ip, dest_ip", "description": "Shows all Suricata alerts in a list by unique source and destination IP addresses" }, { "name": "Suricata Alerts by Subnet", "value": "event_type==\"alert\" | alerts := union(alert.category) by network_of(dest_ip)", "description": "Displays a list of Suricata alerts by CIDR network" } ] } +{ + "name": "Brimcap", + "items": [ + { + "name": "Activity Overview", + "value": "count() by _path | sort -r", + "description": "Shows a list of all Zeek streams in the data set, with a count of associated records" + }, + { + "name": "Unique DNS Queries", + "value": "_path==\"dns\" | count() by query | sort -r", + "description": "Shows all unique DNS queries in the data set with count" + }, + { + "name": "Windows Networking Activity", + "value": "grep(smb*,_path) OR _path==\"dce_rpc\"", + "description": "Filters and displays smb_files, smb_mapping and DCE_RPC activity" + }, + { + "name": "HTTP Requests", + "value": "_path==\"http\" | cut id.orig_h, id.resp_h, id.resp_p, method, host, uri | uniq -c", + "description": "Displays a list of the count of unique HTTP requests including source and destination" + }, + { + "name": "Unique Network Connections", + "value": "_path==\"conn\" | cut id.orig_h, id.resp_p, id.resp_h | sort | uniq", + "description": "Displays a table showing all unique source:port:destination connections pairings" + }, + { + "name": "Connection Received Data", + "value": "_path==\"conn\" | put total_bytes := orig_bytes + resp_bytes | sort -r total_bytes | cut uid, id, orig_bytes, resp_bytes, total_bytes", + "description": "Shows the connections between hosts, sorted by data received" + }, + { + "name": "File Activity", + "value": "filename!=null | cut _path, tx_hosts, rx_hosts, conn_uids, mime_type, filename, md5, sha1", + "description": "Displays a curated view of file data including md5 and sha1 for complete file transfers" + }, + { + "name": "HTTP Post Requests", + "value": "method==\"POST\" | cut ts, uid, id, method, uri, status_code", + "description": "Displays all HTTP Post requests including the URI and HTTP status code" + }, + { + "name": "Show IP Subnets", + "value": "_path==\"conn\" | put classnet := network_of(id.resp_h) | cut classnet | count() by classnet | sort -r", + "description": "Enumerates the classful networks for all destination IP addresses including count of connections" + }, + { + "name": "Suricata Alerts by Category", + "value": "event_type==\"alert\" | count() by alert.severity,alert.category | sort count", + "description": "Shows all Suricata alert counts by category and severity" + }, + { + "name": "Suricata Alerts by Source and Destination", + "value": "event_type==\"alert\" | alerts := union(alert.category) by src_ip, dest_ip", + "description": "Shows all Suricata alerts in a list by unique source and destination IP addresses" + }, + { + "name": "Suricata Alerts by Subnet", + "value": "event_type==\"alert\" | alerts := union(alert.category) by network_of(dest_ip)", + "description": "Displays a list of Suricata alerts by CIDR network" + } + ] +} diff --git a/yarn.lock b/yarn.lock index 79477d71bf..7561b6d167 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1949,41 +1949,6 @@ __metadata: languageName: unknown linkType: soft -"@chevrotain/cst-dts-gen@npm:10.5.0": - version: 10.5.0 - resolution: "@chevrotain/cst-dts-gen@npm:10.5.0" - dependencies: - "@chevrotain/gast": 10.5.0 - "@chevrotain/types": 10.5.0 - lodash: 4.17.21 - checksum: 3ff851d5cbccc509269bb77078dafd7acfcd2e128e7d362718cde728f3fa95f4dd58eb1eea67ecf11453fba70bded97df55c5ba31ed93fb2dec4324663bd2eee - languageName: node - linkType: hard - -"@chevrotain/gast@npm:10.5.0": - version: 10.5.0 - resolution: "@chevrotain/gast@npm:10.5.0" - dependencies: - "@chevrotain/types": 10.5.0 - lodash: 4.17.21 - checksum: 35183e7067bc936db9ecfea7624ee3178634618cf1518ea3470b4ed208fb19454dc3ed990a0de2dab80794251398a857ad17d26cc552eac497a2aa974f76b86d - languageName: node - linkType: hard - -"@chevrotain/types@npm:10.5.0": - version: 10.5.0 - resolution: "@chevrotain/types@npm:10.5.0" - checksum: 72f7b48de1888ab14831108da4b0ab3ef244e1101a4094240382e4983a9e71aae6f8a87e09b819854d1028cee08f97b7d2a81fce935742c55d2bc497b7cad350 - languageName: node - linkType: hard - -"@chevrotain/utils@npm:10.5.0": - version: 10.5.0 - resolution: "@chevrotain/utils@npm:10.5.0" - checksum: f3ae9e0fea2e928a1a4930311d3ef04f45c29fa58ba4d5d2ca43c33355ac47f95ce99a98d6496706e2e7f773ef684a9a7e7cbd7b77c00af9158f08c82d88212b - languageName: node - linkType: hard - "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -7153,14 +7118,14 @@ __metadata: languageName: node linkType: hard -"bullet@npm:^0.0.2": - version: 0.0.2 - resolution: "bullet@npm:0.0.2" +"bullet@npm:^0.0.7": + version: 0.0.7 + resolution: "bullet@npm:0.0.7" dependencies: "@reduxjs/toolkit": ^2.2.5 lodash-es: ^4.17.21 pluralize: ^8.0.0 - checksum: 9e99282d52e346f2f7612a581f65d3693c2aa9ddd9ac5352abf7990d79c34748d1d196d88e9fcc96243d9d286a7e25d876d174e93d99a8519598c36a9be85ae5 + checksum: 068df123e5bb1160d3294d2a2d46db8e28d564f78f5bcc88a96cb06d7acbc33878f7e1e9b6f06d40b43976c57ecc366ed9879e522cd032bcc6c8ade0d9f44889 languageName: node linkType: hard @@ -7424,20 +7389,6 @@ __metadata: languageName: node linkType: hard -"chevrotain@npm:^10.5.0": - version: 10.5.0 - resolution: "chevrotain@npm:10.5.0" - dependencies: - "@chevrotain/cst-dts-gen": 10.5.0 - "@chevrotain/gast": 10.5.0 - "@chevrotain/types": 10.5.0 - "@chevrotain/utils": 10.5.0 - lodash: 4.17.21 - regexp-to-ast: 0.5.0 - checksum: b641f149f60979a29eff2434d745e9565a7c89422b601d554bcf8f047f7d8ff776b9a54b1b36085a622e3f1ed7eb4b8721b5a5348d90ae2567ce7594b10f25aa - languageName: node - linkType: hard - "chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.0, chokidar@npm:^3.4.2, chokidar@npm:^3.5.2": version: 3.5.3 resolution: "chokidar@npm:3.5.3" @@ -12865,7 +12816,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4.17.21, lodash@npm:^4.17.11, lodash@npm:^4.17.15, lodash@npm:^4.17.21": +"lodash@npm:^4.17.11, lodash@npm:^4.17.15, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -15619,13 +15570,6 @@ __metadata: languageName: node linkType: hard -"regexp-to-ast@npm:0.5.0": - version: 0.5.0 - resolution: "regexp-to-ast@npm:0.5.0" - checksum: 72e32f2a1217bb22398ac30867ddd43e16943b6b569dd4eb472de47494c7a39e34f47ee3e92ad4cbf92308f98997da366fe094a0e58eb6b93eab0ee956fff86d - languageName: node - linkType: hard - "regexp.prototype.flags@npm:^1.4.3": version: 1.5.0 resolution: "regexp.prototype.flags@npm:1.5.0" @@ -18300,6 +18244,13 @@ __metadata: languageName: node linkType: hard +"when-clause@npm:^0.0.4": + version: 0.0.4 + resolution: "when-clause@npm:0.0.4" + checksum: 711a2a281a47d672923f9a4f2bc6c2c9ff8845f5a8463a6b7155d099bd385aec292e780010ad00bd1505326d189e0096cbb72771658c86dbef90bfc5a7d0832b + languageName: node + linkType: hard + "which-boxed-primitive@npm:^1.0.2": version: 1.0.2 resolution: "which-boxed-primitive@npm:1.0.2" @@ -18716,9 +18667,8 @@ __metadata: ajv: ^6.9.1 animejs: ^3.2.0 brimcap: "brimdata/brimcap#v1.18.0" - bullet: ^0.0.2 + bullet: ^0.0.7 chalk: ^4.1.0 - chevrotain: ^10.5.0 chrono-node: ^2.5.0 classnames: ^2.2.6 commander: ^2.20.3 @@ -18802,6 +18752,7 @@ __metadata: utopia-core-scss: ^1.0.1 web-file-polyfill: ^1.0.4 web-streams-polyfill: ^3.2.0 + when-clause: ^0.0.4 zed: "brimdata/zed#65b575c5ff9a95c7c2bf7dd9ceb935b1a51d7676" zui-test-data: "workspace:*" peerDependencies: