-
-
-
-
+
+
- {error && {formatError(error)}
}
-
-
-
-
-
-
+
+
+
)
}
diff --git a/apps/zui/src/views/pool-form/data.ts b/apps/zui/src/views/pool-form/data.ts
new file mode 100644
index 0000000000..efebefc9e4
--- /dev/null
+++ b/apps/zui/src/views/pool-form/data.ts
@@ -0,0 +1,8 @@
+export function getPoolFormData(form: HTMLFormElement) {
+ const data = new FormData(form)
+ return {
+ name: data.get("name"),
+ key: data.get("key"),
+ order: data.get("order"),
+ }
+}
diff --git a/apps/zui/src/views/pool-form/index.tsx b/apps/zui/src/views/pool-form/index.tsx
new file mode 100644
index 0000000000..39d4a6b239
--- /dev/null
+++ b/apps/zui/src/views/pool-form/index.tsx
@@ -0,0 +1,54 @@
+import forms from "src/components/forms.module.css"
+
+type Props = {
+ nameInput?: any
+ keyInput?: any
+}
+
+export function PoolForm(props: Props) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/zui/src/views/pool-page/details.module.css b/apps/zui/src/views/pool-page/details.module.css
index 38de5cf621..1c881ab741 100644
--- a/apps/zui/src/views/pool-page/details.module.css
+++ b/apps/zui/src/views/pool-page/details.module.css
@@ -1,17 +1,6 @@
-.details {
- padding: var(--page-padding);
- max-width: var(--page-max-width);
- width: 100%;
- margin: 0 auto;
-}
-
-.title {
- margin-bottom: 1em;
-}
-
.list {
- background: var(--chrome-color);
- padding: 1rem;
- border-radius: 16px;
- max-width: max-content;
+ background: var(--chrome-color);
+ padding: 1rem;
+ border-radius: 16px;
+ max-width: max-content;
}
diff --git a/apps/zui/src/views/pool-page/details.tsx b/apps/zui/src/views/pool-page/details.tsx
index 14ffab9715..8a2ec1e700 100644
--- a/apps/zui/src/views/pool-page/details.tsx
+++ b/apps/zui/src/views/pool-page/details.tsx
@@ -6,8 +6,8 @@ import classNames from "classnames"
export function Details({pool}: {pool: Pool}) {
const keys = pool.keys.map((k) => (k ? k.join(".") : "null"))
return (
-
- Pool Details
+
+ Pool Details
- ID
diff --git a/apps/zui/src/views/pool-page/index.tsx b/apps/zui/src/views/pool-page/index.tsx
index 6eca43e8ad..d60de2dfeb 100644
--- a/apps/zui/src/views/pool-page/index.tsx
+++ b/apps/zui/src/views/pool-page/index.tsx
@@ -23,8 +23,8 @@ const Toolbar = styled.div`
`
const Subtitle = styled.p`
- font-size: 13px;
- opacity: 0.5;
+ font-size: var(--step-0);
+ color: var(--fg-color-less);
margin: 0;
`
@@ -65,7 +65,7 @@ export const Show = () => {
-
+
diff --git a/apps/zui/src/views/pool-page/job.module.css b/apps/zui/src/views/pool-page/job.module.css
index 178b7116ef..0c23ab41af 100644
--- a/apps/zui/src/views/pool-page/job.module.css
+++ b/apps/zui/src/views/pool-page/job.module.css
@@ -1,111 +1,111 @@
.job {
- display: grid;
- grid-template-columns: 3rem 1fr min-content;
- grid-template-rows: min-content min-content;
- align-content: center;
- align-items: center;
- column-gap: 1em;
- grid-template-areas: "icon info actions"
- ". details .";
- min-height: 86px;
+ display: grid;
+ grid-template-columns: 3rem 1fr min-content;
+ grid-template-rows: min-content min-content;
+ align-content: center;
+ align-items: center;
+ column-gap: 1em;
+ grid-template-areas:
+ "icon info actions"
+ ". details .";
+ min-height: 86px;
}
.icon {
- grid-area: icon;
+ grid-area: icon;
}
.info {
- grid-area: info;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- gap: 0.25em;
+ grid-area: info;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25em;
}
.actions {
- grid-area: actions;
+ grid-area: actions;
}
.name {
- font-weight: bold;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
+ font-weight: bold;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
}
.progress {
- grid-area: progress;
- display: flex;
- gap: 0.25em;
+ grid-area: progress;
+ display: flex;
+ gap: 0.25em;
}
.status {
- grid-area: status;
- color: var(--fg-color-less);
- font-size: 0.8rem;
+ grid-area: status;
+ color: var(--fg-color-less);
+ font-size: var(--step--1);
}
.loadingIcon,
.successIcon,
.errorIcon {
- width: 3rem;
- height: 3rem;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
+ width: 3rem;
+ height: 3rem;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.loadingIcon path:nth-child(1) {
- animation: upload 1.6s 532ms infinite var(--pop-easing);
+ animation: upload 1.6s 532ms infinite var(--pop-easing);
}
.loadingIcon path:nth-child(2) {
- animation: upload 1.6s 266ms infinite var(--pop-easing);
+ animation: upload 1.6s 266ms infinite var(--pop-easing);
}
.loadingIcon path:nth-child(3) {
- animation: upload 1.6s infinite var(--pop-easing);
+ animation: upload 1.6s infinite var(--pop-easing);
}
@keyframes upload {
- from {
- transform: translateY(80%);
- opacity: 0;
- }
+ from {
+ transform: translateY(80%);
+ opacity: 0;
+ }
- 50% {
- opacity: 1;
- transform: translateY(0);
- }
-
- to {
- opacity: 0;
- transform: translateY(-80%);
- }
+ 50% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(-80%);
+ }
}
.loadingIcon {
- color: var(--fg-color-less);
- color: var(--blue);
- background: var(--chrome-color-more);
+ color: var(--fg-color-less);
+ color: var(--blue);
+ background: var(--chrome-color-more);
}
.successIcon {
- color: white;
- background: var(--success-color);
+ color: white;
+ background: var(--success-color);
}
.errorIcon {
- color: white;
- background: var(--error-color);
+ color: white;
+ background: var(--error-color);
}
.details {
- border-top: 1px solid var(--border-color);
- margin-top: 0.5rem;
- padding-top: 0.75rem;
- color: var(--fg-color-less);
- grid-area: details;
+ border-top: 1px solid var(--border-color);
+ margin-top: 0.5rem;
+ padding-top: 0.75rem;
+ color: var(--fg-color-less);
+ grid-area: details;
}
diff --git a/apps/zui/src/views/pool-page/recent-loads.module.css b/apps/zui/src/views/pool-page/recent-loads.module.css
index 23bc63813f..ff6e165095 100644
--- a/apps/zui/src/views/pool-page/recent-loads.module.css
+++ b/apps/zui/src/views/pool-page/recent-loads.module.css
@@ -1,28 +1,10 @@
-.recentLoads {
- padding: var(--page-padding);
- padding-top: calc(var(--page-padding) * 1.5);
- max-width: var(--page-max-width);
- margin: 0 auto;
- border-radius: 10px;
+.list > * {
+ padding: var(--s1);
+ border-radius: 16px;
+ cursor: default;
+ background: var(--chrome-color);
}
-.title {
- margin-bottom: 1em;
-}
-
-.list {
- display: flex;
- flex-direction: column;
- gap: .5rem;
-}
-
-.list>* {
- padding: 1rem;
- border-radius: 16px;
- cursor: default;
- background: var(--chrome-color);
-}
-
-.list>*:hover {
- background: var(--emphasis-bg-less);
+.list > *:hover {
+ background: var(--emphasis-bg-less);
}
diff --git a/apps/zui/src/views/pool-page/recent-loads.tsx b/apps/zui/src/views/pool-page/recent-loads.tsx
index 7337eef1da..731d3e67b5 100644
--- a/apps/zui/src/views/pool-page/recent-loads.tsx
+++ b/apps/zui/src/views/pool-page/recent-loads.tsx
@@ -8,17 +8,17 @@ import {invoke} from "src/core/invoke"
import {LoadModel} from "src/domain/loads/load-model"
import * as fmt from "date-fns"
import {ErrorLines} from "src/components/errors-lines"
+import classNames from "classnames"
export function RecentLoads(props: {id: string}) {
const loads = useSelector((s: State) => Loads.wherePoolId(s, props.id))
const dispatch = useDispatch()
const cancelLoad = async (load) => {
- if (
- load.status == "loading" &&
- confirm("Are you sure you want to abort this load?")
- ) {
- await invoke("loads.abort", load.id)
+ if (load.status == "loading") {
+ if (confirm("Are you sure you want to abort this load?")) {
+ await invoke("loads.abort", load.id)
+ }
} else {
dispatch(Loads.delete(load.id))
}
@@ -27,16 +27,16 @@ export function RecentLoads(props: {id: string}) {
if (loads.length === 0) return null
return (
-
- Recent Loads
-
+
+ Recent Loads
+
{loads
.map((ref) => new LoadModel(ref))
.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())
.map((load) => (
+
+
+
+ )
+}
diff --git a/apps/zui/src/views/preferences-modal/index.tsx b/apps/zui/src/views/preferences-modal/index.tsx
index 1c2c23a714..dd06ff1f8d 100644
--- a/apps/zui/src/views/preferences-modal/index.tsx
+++ b/apps/zui/src/views/preferences-modal/index.tsx
@@ -5,35 +5,22 @@ import useCallbackRef from "src/js/components/hooks/useCallbackRef"
import {Form} from "./form"
import forms from "src/components/forms.module.css"
import classNames from "classnames"
-import {Dialog} from "src/components/dialog"
-import {Debut, useDebut} from "src/components/debut"
-import modals from "src/components/modals.module.css"
import styles from "./index.module.css"
-import Modal from "src/js/state/Modal"
-import {useSelector} from "react-redux"
-import {useDispatch} from "src/app/core/state"
import {H1} from "src/components/h1"
import {useFields} from "./use-fields"
import {objectIsEmpty} from "src/util/object-is-empty"
+import {PopoverModal, usePopoverModal} from "src/components/popover-modal"
-export function PreferencesModal() {
- const name = useSelector(Modal.getName)
- if (name !== "settings") return null
-
- return
-}
-
-export function ModalWithFields() {
+export function SettingsModal() {
const fields = useFields()
if (objectIsEmpty(fields)) return null
- return
+ return
}
-export function ModalDialog(props: {fields: FormConfig}) {
- const dispatch = useDispatch()
+function Content(props: {fields: FormConfig}) {
+ const modal = usePopoverModal()
const [formEl, setForm] = useCallbackRef()
const [errors, setErrors] = useState([])
- const debut = useDebut({afterExit: () => dispatch(Modal.hide())})
const onSubmit = useCallback(
async (e) => {
@@ -44,7 +31,7 @@ export function ModalDialog(props: {fields: FormConfig}) {
if (await f.isValid()) {
setErrors([])
f.submit()
- debut.exit()
+ modal.close()
} else {
setErrors(f.getErrors())
}
@@ -52,39 +39,35 @@ export function ModalDialog(props: {fields: FormConfig}) {
[formEl, props.fields]
)
- const onCancel = () => {
- debut.exit()
- }
-
return (
-
-
)
@@ -91,19 +96,19 @@ export function RowCount() {
} else if (status === "COMPLETE") {
return (
- {count} {pluralize("Row", count)}
+ {count?.toLocaleString()} {pluralize("Row", count)}
)
} else if (status === "INCOMPLETE") {
return (
- First {count} {pluralize("Row", count)}
+ First {count?.toLocaleString()} {pluralize("Row", count)}
)
} else if (status === "LIMIT") {
return (
- Limited to {count} {pluralize("Row", count)}
+ Limited to {count?.toLocaleString()} {pluralize("Row", count)}
)
}
@@ -116,10 +121,25 @@ function ShapeCount() {
if (["COMPLETE", "LIMIT", "INCOMPLETE"].includes(status)) {
return (
- {count} {pluralize("Shape", count)}
+ {count?.toLocaleString()} {pluralize("Shape", count)}
)
} else {
return null
}
}
+
+function TotalCount() {
+ const status = useSelector(Results.getStatus(RESULTS_QUERY_COUNT))
+ const values = useSelector(Results.getValues(RESULTS_QUERY_COUNT))
+ const count = values[0]?.toJS()
+ if (["COMPLETE", "LIMIT", "INCOMPLETE"].includes(status)) {
+ return (
+
+ {count?.toLocaleString()} Total {pluralize("Row", count)}
+
+ )
+ } else {
+ return Fetching Total Rows...
+ }
+}
diff --git a/apps/zui/src/views/session-page/loader.ts b/apps/zui/src/views/session-page/loader.ts
index 1d2f67bafc..946d452e00 100644
--- a/apps/zui/src/views/session-page/loader.ts
+++ b/apps/zui/src/views/session-page/loader.ts
@@ -9,7 +9,10 @@ import {Location} from "history"
import Pools from "src/js/state/Pools"
import {invoke} from "src/core/invoke"
import {runHistogramQuery} from "src/views/histogram-pane/run-query"
-import {runResultsQuery} from "src/views/results-pane/run-results-query"
+import {
+ runResultsCount,
+ runResultsMain,
+} from "src/views/results-pane/run-results-query"
import Layout from "src/js/state/Layout"
import {syncPool} from "src/app/core/pools/sync-pool"
@@ -51,7 +54,8 @@ function fetchData() {
startTransition(() => {
if (version) {
- dispatch(runResultsQuery())
+ runResultsMain()
+ runResultsCount()
if (histogramVisible) {
runHistogramQuery(api)
}
diff --git a/apps/zui/src/views/session-page/pins.module.css b/apps/zui/src/views/session-page/pins.module.css
index 667aa20265..a741acdb54 100644
--- a/apps/zui/src/views/session-page/pins.module.css
+++ b/apps/zui/src/views/session-page/pins.module.css
@@ -12,7 +12,7 @@
display: relative;
font-family: var(--mono-font);
font-weight: 500;
- font-size: 0.75rem;
+ font-size: var(--step--1);
background: var(--chrome-color);
border: none;
padding: var(--s-5) var(--s-1);
diff --git a/apps/zui/src/views/session-page/pins/dialog.tsx b/apps/zui/src/views/session-page/pins/dialog.tsx
index 83a75daed1..7271cee854 100644
--- a/apps/zui/src/views/session-page/pins/dialog.tsx
+++ b/apps/zui/src/views/session-page/pins/dialog.tsx
@@ -23,7 +23,7 @@ export type DialogProps = {
const BG = styled.dialog`
border: none;
- box-shadow: var(--shadow-medium);
+ box-shadow: var(--shadow-elevation-medium);
border-radius: 6px;
background: var(--bg-color);
color: var(--fg-color);
diff --git a/apps/zui/src/views/session-page/pins/generic-pin-form.tsx b/apps/zui/src/views/session-page/pins/generic-pin-form.tsx
index bdcccd1a72..aabb88f163 100644
--- a/apps/zui/src/views/session-page/pins/generic-pin-form.tsx
+++ b/apps/zui/src/views/session-page/pins/generic-pin-form.tsx
@@ -1,16 +1,7 @@
import React from "react"
import {GenericQueryPin} from "src/js/state/Editor/types"
import {PinFormProps} from "./base-pin"
-import {
- Actions,
- Field,
- getFormData,
- Input,
- Label,
- TextArea,
- RedLink,
- ActionsGroup,
-} from "./form-helpers"
+import {getFormData, TextArea, RedLink} from "./form-helpers"
import forms from "src/components/forms.module.css"
export function GenericPinForm(props: PinFormProps) {
@@ -21,33 +12,35 @@ export function GenericPinForm(props: PinFormProps) {
onSubmit={(e) => props.onSubmit(getFormData(e))}
onReset={props.onReset}
>
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
Delete
-
-
-
-
-
-
+
+
+
+
+
+
)
}
diff --git a/apps/zui/src/zui-kit/react/list-view.tsx b/apps/zui/src/zui-kit/react/list-view.tsx
index 233e8d7c8c..8a6bce5c57 100644
--- a/apps/zui/src/zui-kit/react/list-view.tsx
+++ b/apps/zui/src/zui-kit/react/list-view.tsx
@@ -17,7 +17,10 @@ import {useStateControllers} from "./use-state-controllers"
import {mergeRefs, useInitialScrollPosition, useOnScroll} from "./utils"
import classNames from "classnames"
import {useParentSize} from "src/app/core/hooks/use-parent-size"
-import {TopShadow, useScrollShadow} from "src/views/load-pane/scroll-shadow"
+import {
+ TopShadow,
+ useScrollShadow,
+} from "src/views/preview-load-modal/scroll-shadow"
import {call} from "src/util/call"
import {config} from "src/components/zed-table/config"
diff --git a/apps/zui/src/zui/lake.ts b/apps/zui/src/zui/lake.ts
index 77f9e160f6..20f16c6950 100644
--- a/apps/zui/src/zui/lake.ts
+++ b/apps/zui/src/zui/lake.ts
@@ -3,6 +3,7 @@ import {Client} from "@brimdata/zed-node"
class LakeApi {
public client: Client | null = null
+ public id: string | null
query(program: string, options?: QueryOpts) {
if (!this.client) throw new Error("No client configured for this lake")
diff --git a/packages/zui-player/helpers/test-app.ts b/packages/zui-player/helpers/test-app.ts
index d8d1c266c0..514621a2d7 100644
--- a/packages/zui-player/helpers/test-app.ts
+++ b/packages/zui-player/helpers/test-app.ts
@@ -194,6 +194,10 @@ export default class TestApp {
return this.page.keyboard.press(key);
}
+ async select(label: string, value: string) {
+ return this.page.getByLabel(label).selectOption(value);
+ }
+
locate(role: Role | RegExp, name?: string) {
if (role instanceof RegExp) {
return this.mainWin.getByText(role).first();
@@ -226,7 +230,10 @@ export default class TestApp {
}
async takeScreenshot(filename: string) {
- return await this.page.screenshot({ path: path.join('run', 'screenshots', filename), fullPage: true });
+ return await this.page.screenshot({
+ path: path.join('run', 'screenshots', filename),
+ fullPage: true,
+ });
}
}
diff --git a/packages/zui-player/tests/export.spec.ts b/packages/zui-player/tests/export.spec.ts
index f9bcd64c87..f98e2c3794 100644
--- a/packages/zui-player/tests/export.spec.ts
+++ b/packages/zui-player/tests/export.spec.ts
@@ -23,7 +23,6 @@ test.describe('Export tests', () => {
const app = new TestApp('Export tests');
test.beforeAll(async () => {
-
// Increase timeout due to observed long load times on test data in CI.
// See https://github.com/brimdata/zui/pull/2967
test.setTimeout(60000);
@@ -48,10 +47,11 @@ test.describe('Export tests', () => {
await app.click('button', 'Export Results');
await app.attached('dialog');
const dialog = app.mainWin.getByRole('dialog');
+ await app.select('Format', label);
await dialog
- .getByRole('radio', { name: `${label}`, exact: true })
+ .getByRole('button')
+ .filter({ hasText: 'Export To File' })
.click();
- await dialog.getByRole('button').filter({ hasText: 'Export' }).click();
await app.detached('dialog');
await app.mainWin
.getByText(new RegExp('Export Completed: .*results\\.' + label))
@@ -60,4 +60,28 @@ test.describe('Export tests', () => {
expect(fsExtra.statSync(file).size).toBe(expectedSize);
});
});
+
+ test('Copy to clipboard', async () => {
+ await app.click('button', 'Export Results');
+ await app.attached('dialog');
+ await app.select('Format', 'JSON');
+ await app.click(/Copy to Clipboard/i);
+ await app.attached(/copied JSON data to clipboard/i);
+ await app.click('button', 'Close');
+ });
+
+ test('Export to Pool', async () => {
+ await app.query('head 5');
+ await app.click('button', 'Export Results');
+ await app.attached('dialog');
+ await app.locate('radio', 'Pool').check();
+ await app.fill('Name', 'five_row_pool');
+ await app.click('button', 'Export To Pool');
+ // Redirected to the new pool page
+ await app.attached(/from 'sample.zeektsv' | head 5/);
+ await app.attached('heading', 'five_row_pool');
+ // Ensure the records are present
+ await app.click('button', 'Query Pool');
+ await app.attached(/5 Total Rows/);
+ });
});
diff --git a/packages/zui-player/tests/preview-and-load.spec.ts b/packages/zui-player/tests/preview-and-load.spec.ts
index 667f307d1d..b3ea6d84f8 100644
--- a/packages/zui-player/tests/preview-and-load.spec.ts
+++ b/packages/zui-player/tests/preview-and-load.spec.ts
@@ -4,7 +4,6 @@ import { getPath } from 'zui-test-data';
play('Preview & Load', (app, test) => {
test('create new pool, change key, type ', async () => {
await app.dropFile(getPath('sample.zeektsv'));
- await app.click('button', 'Pool Settings');
await app.fill('Pool Key', 'my_new_key');
await app.press('Enter');