From 4284901d741b38f730f8bc01cc643f8f55ad66d6 Mon Sep 17 00:00:00 2001 From: "release-feldera-feldera[bot]" Date: Mon, 6 Apr 2026 08:09:31 +0000 Subject: [PATCH 1/4] ci: Prepare for v0.283.0 --- Cargo.lock | 40 ++++++++++++++++++++-------------------- Cargo.toml | 24 ++++++++++++------------ openapi.json | 2 +- python/pyproject.toml | 2 +- python/uv.lock | 4 ++-- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 179949b262..ab34665f41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3761,7 +3761,7 @@ dependencies = [ [[package]] name = "dbsp" -version = "0.282.0" +version = "0.283.0" dependencies = [ "anyhow", "arc-swap", @@ -3849,7 +3849,7 @@ dependencies = [ [[package]] name = "dbsp_adapters" -version = "0.282.0" +version = "0.283.0" dependencies = [ "actix", "actix-codec", @@ -3986,7 +3986,7 @@ dependencies = [ [[package]] name = "dbsp_nexmark" -version = "0.282.0" +version = "0.283.0" dependencies = [ "anyhow", "ascii_table", @@ -4861,7 +4861,7 @@ dependencies = [ [[package]] name = "fda" -version = "0.282.0" +version = "0.283.0" dependencies = [ "anyhow", "arrow", @@ -4913,7 +4913,7 @@ dependencies = [ [[package]] name = "feldera-adapterlib" -version = "0.282.0" +version = "0.283.0" dependencies = [ "actix-web", "anyhow", @@ -4944,7 +4944,7 @@ dependencies = [ [[package]] name = "feldera-buffer-cache" -version = "0.282.0" +version = "0.283.0" dependencies = [ "crossbeam-utils", "enum-map", @@ -4972,7 +4972,7 @@ dependencies = [ [[package]] name = "feldera-datagen" -version = "0.282.0" +version = "0.283.0" dependencies = [ "anyhow", "async-channel 2.5.0", @@ -4998,7 +4998,7 @@ dependencies = [ [[package]] name = "feldera-fxp" -version = "0.282.0" +version = "0.283.0" dependencies = [ "bytecheck", "dbsp", @@ -5018,7 +5018,7 @@ dependencies = [ [[package]] name = "feldera-iceberg" -version = "0.282.0" +version = "0.283.0" dependencies = [ "anyhow", "chrono", @@ -5038,7 +5038,7 @@ dependencies = [ [[package]] name = "feldera-ir" -version = "0.282.0" +version = "0.283.0" dependencies = [ "proptest", "proptest-derive", @@ -5050,7 +5050,7 @@ dependencies = [ [[package]] name = "feldera-macros" -version = "0.282.0" +version = "0.283.0" dependencies = [ "prettyplease", "proc-macro2", @@ -5060,7 +5060,7 @@ dependencies = [ [[package]] name = "feldera-observability" -version = "0.282.0" +version = "0.283.0" dependencies = [ "actix-http", "awc", @@ -5075,7 +5075,7 @@ dependencies = [ [[package]] name = "feldera-rest-api" -version = "0.282.0" +version = "0.283.0" dependencies = [ "chrono", "feldera-observability", @@ -5109,7 +5109,7 @@ dependencies = [ [[package]] name = "feldera-sqllib" -version = "0.282.0" +version = "0.283.0" dependencies = [ "arcstr", "base58", @@ -5150,7 +5150,7 @@ dependencies = [ [[package]] name = "feldera-storage" -version = "0.282.0" +version = "0.283.0" dependencies = [ "anyhow", "crossbeam", @@ -5173,7 +5173,7 @@ dependencies = [ [[package]] name = "feldera-types" -version = "0.282.0" +version = "0.283.0" dependencies = [ "actix-web", "anyhow", @@ -8094,7 +8094,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pipeline-manager" -version = "0.282.0" +version = "0.283.0" dependencies = [ "actix-cors", "actix-files", @@ -9188,7 +9188,7 @@ dependencies = [ [[package]] name = "readers" -version = "0.282.0" +version = "0.283.0" dependencies = [ "async-std", "csv", @@ -10764,7 +10764,7 @@ dependencies = [ [[package]] name = "sltsqlvalue" -version = "0.282.0" +version = "0.283.0" dependencies = [ "dbsp", "feldera-sqllib", @@ -11067,7 +11067,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "storage-test-compat" -version = "0.282.0" +version = "0.283.0" dependencies = [ "dbsp", "derive_more 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index 10d170e216..6e21944160 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] authors = ["Feldera Team "] -version = "0.282.0" +version = "0.283.0" license = "MIT OR Apache-2.0" homepage = "https://github.com/feldera/feldera" repository = "https://github.com/feldera/feldera" @@ -101,7 +101,7 @@ csv = "1.2.2" csv-core = "0.1.10" dashmap = "6.1.0" datafusion = "51.0" -dbsp = { path = "crates/dbsp", version = "0.282.0" } +dbsp = { path = "crates/dbsp", version = "0.283.0" } dbsp_nexmark = { path = "crates/nexmark" } deadpool-postgres = "0.14.1" #deltalake = "0.30.2" @@ -121,19 +121,19 @@ erased-serde = "0.3.31" fake = "2.10" fastbloom = "0.14.0" fdlimit = "0.3.0" -feldera-buffer-cache = { version = "0.282.0", path = "crates/buffer-cache" } +feldera-buffer-cache = { version = "0.283.0", path = "crates/buffer-cache" } feldera-cloud1-client = "0.1.2" feldera-datagen = { path = "crates/datagen" } -feldera-fxp = { version = "0.282.0", path = "crates/fxp", features = ["dbsp"] } +feldera-fxp = { version = "0.283.0", path = "crates/fxp", features = ["dbsp"] } feldera-iceberg = { path = "crates/iceberg" } -feldera-observability = { version = "0.282.0", path = "crates/feldera-observability" } -feldera-macros = { version = "0.282.0", path = "crates/feldera-macros" } -feldera-sqllib = { version = "0.282.0", path = "crates/sqllib" } -feldera-storage = { version = "0.282.0", path = "crates/storage" } -feldera-types = { version = "0.282.0", path = "crates/feldera-types" } -feldera-rest-api = { version = "0.282.0", path = "crates/rest-api" } -feldera-ir = { version = "0.282.0", path = "crates/ir" } -feldera-adapterlib = { version = "0.282.0", path = "crates/adapterlib" } +feldera-observability = { version = "0.283.0", path = "crates/feldera-observability" } +feldera-macros = { version = "0.283.0", path = "crates/feldera-macros" } +feldera-sqllib = { version = "0.283.0", path = "crates/sqllib" } +feldera-storage = { version = "0.283.0", path = "crates/storage" } +feldera-types = { version = "0.283.0", path = "crates/feldera-types" } +feldera-rest-api = { version = "0.283.0", path = "crates/rest-api" } +feldera-ir = { version = "0.283.0", path = "crates/ir" } +feldera-adapterlib = { version = "0.283.0", path = "crates/adapterlib" } flate2 = "1.1.0" form_urlencoded = "1.2.0" futures = "0.3.30" diff --git a/openapi.json b/openapi.json index 3b951bec1a..9be5a505c9 100644 --- a/openapi.json +++ b/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "MIT OR Apache-2.0" }, - "version": "0.282.0" + "version": "0.283.0" }, "paths": { "/config/authentication": { diff --git a/python/pyproject.toml b/python/pyproject.toml index 6d2d94693f..2877cbf739 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "feldera" readme = "README.md" description = "The feldera python client" -version = "0.282.0" +version = "0.283.0" license = "MIT" requires-python = ">=3.10" authors = [ diff --git a/python/uv.lock b/python/uv.lock index 036a757848..c0f036e7d8 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -12,7 +12,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-03-28T07:51:53.482034304Z" +exclude-newer = "2026-03-30T08:09:04.231678467Z" exclude-newer-span = "P1W" [[package]] @@ -221,7 +221,7 @@ wheels = [ [[package]] name = "feldera" -version = "0.282.0" +version = "0.283.0" source = { editable = "." } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, From b5f753ead85100922ae0674ab34bc8f318808f94 Mon Sep 17 00:00:00 2001 From: Anand Raman Date: Thu, 2 Apr 2026 13:16:47 -0500 Subject: [PATCH 2/4] [UI] implement pipeline search Added a search feature to the UI to filter pipelines by name Signed-off-by: Anand Raman updated tests --- .../src/lib/components/pipelines/Table.svelte | 31 ++-- .../pipelines/table/AvailableActions.svelte | 12 +- .../src/lib/functions/common/string.spec.ts | 40 ++++++ .../src/lib/functions/common/string.ts | 8 ++ .../web-console/tests/pipelineSearch.e2e.ts | 132 ++++++++++++++++++ 5 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 js-packages/web-console/src/lib/functions/common/string.spec.ts create mode 100644 js-packages/web-console/tests/pipelineSearch.e2e.ts diff --git a/js-packages/web-console/src/lib/components/pipelines/Table.svelte b/js-packages/web-console/src/lib/components/pipelines/Table.svelte index 6ab9585b79..d228667473 100644 --- a/js-packages/web-console/src/lib/components/pipelines/Table.svelte +++ b/js-packages/web-console/src/lib/components/pipelines/Table.svelte @@ -9,6 +9,7 @@ import PipelineStatus from '$lib/components/pipelines/list/PipelineStatus.svelte' import ThSort from '$lib/components/pipelines/table/ThSort.svelte' import { dateMax } from '$lib/functions/common/date' + import { matchesSubstring } from '$lib/functions/common/string' import { type NamesInUnion, unionName } from '$lib/functions/common/union' import { formatDateTime, useElapsedTime } from '$lib/functions/format' import type { @@ -38,7 +39,7 @@ selectBy: 'name' }) $effect(() => { - table.setRows(pipelinesWithLastChange) + table.setRows(pipelinesFiltered) }) $effect(() => { selectedPipelines = table.selected as string[] @@ -67,13 +68,29 @@ ['Compiling', ['Queued', 'CompilingSql', 'SqlCompiled', 'CompilingRust']], ['Failed', ['SystemError', 'SqlError', 'RustError']] ] + + let nameSearch = $state('') + const pipelinesFiltered = $derived( + pipelinesWithLastChange.filter((p) => matchesSubstring(p.name, nameSearch)) + ) + const { formatElapsedTime } = useElapsedTime() const td = 'py-1 text-base border-t-[0.5px]' -
+
+ { + nameSearch = e.currentTarget.value + }} + /> -
- {@render preHeaderEnd?.()} -
+ {@render preHeaderEnd?.()}
@@ -136,7 +151,7 @@ {#each table.rows as pipeline} - - + {/each} diff --git a/js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte b/js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte index 09c686c842..f1ed86c79a 100644 --- a/js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte +++ b/js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte @@ -171,11 +171,13 @@ {/snippet} -
- {#each actions as action} - {@render action()} - {/each} -
+{#if actions.length} +
+ {#each actions as action} + {@render action()} + {/each} +
+{/if} {#snippet deleteDialog()} { + it('returns true when search is empty', () => { + expect(matchesSubstring('any-pipeline', '')).toBe(true) + }) + + it('matches exact name', () => { + expect(matchesSubstring('my-pipeline', 'my-pipeline')).toBe(true) + }) + + it('matches partial name at the start', () => { + expect(matchesSubstring('my-pipeline', 'my-')).toBe(true) + }) + + it('matches partial name in the middle', () => { + expect(matchesSubstring('my-pipeline', 'pipe')).toBe(true) + }) + + it('matches partial name at the end', () => { + expect(matchesSubstring('my-pipeline', 'line')).toBe(true) + }) + + it('is case-insensitive (lowercase search, mixed case text)', () => { + expect(matchesSubstring('My-Pipeline', 'my-pipeline')).toBe(true) + }) + + it('is case-insensitive (uppercase search)', () => { + expect(matchesSubstring('my-pipeline', 'MY-PIPE')).toBe(true) + }) + + it('returns false when there is no match', () => { + expect(matchesSubstring('my-pipeline', 'xyz')).toBe(false) + }) + + it('returns false for partial mismatch', () => { + expect(matchesSubstring('my-pipeline', 'my-pipez')).toBe(false) + }) +}) diff --git a/js-packages/web-console/src/lib/functions/common/string.ts b/js-packages/web-console/src/lib/functions/common/string.ts index 30815474e7..984922c0b8 100644 --- a/js-packages/web-console/src/lib/functions/common/string.ts +++ b/js-packages/web-console/src/lib/functions/common/string.ts @@ -62,6 +62,14 @@ export function humanSize(bytes: number): string { return bytes.toFixed(1) + ' ' + units[u] } +/** + * Case-insensitive substring match. Returns true if `text` contains `search`, + * or if `search` is empty. + */ +export function matchesSubstring(text: string, search: string): boolean { + return !search || text.toLowerCase().includes(search.toLowerCase()) +} + export function nthIndexOf( str: string, substring: string, diff --git a/js-packages/web-console/tests/pipelineSearch.e2e.ts b/js-packages/web-console/tests/pipelineSearch.e2e.ts new file mode 100644 index 0000000000..4be830a9a2 --- /dev/null +++ b/js-packages/web-console/tests/pipelineSearch.e2e.ts @@ -0,0 +1,132 @@ +import { expect, test } from '@playwright/test' +import { client } from '$lib/services/manager/client.gen' +import { + deletePipeline, + getExtendedPipeline, + putPipeline +} from '$lib/services/pipelineManager' + +const API_ORIGIN = (process.env.PLAYWRIGHT_API_ORIGIN ?? 'http://localhost:8080').replace(/\/$/, '') +client.setConfig({ baseUrl: API_ORIGIN }) + +const PREFIX = `test-search-${Date.now()}` +const PIPELINES = [`${PREFIX}-alpha`, `${PREFIX}-beta`, `${PREFIX}-gamma`] as const + +async function cleanupPipelines() { + for (const name of PIPELINES) { + try { + await deletePipeline(name) + } catch { + // Pipeline may not exist + } + } +} + +/** Poll the API until a pipeline reaches "Stopped" status. */ +async function waitForStopped(name: string, timeoutMs = 120_000) { + const start = Date.now() + while (Date.now() - start < timeoutMs) { + const p = await getExtendedPipeline(name) + if (p.status === 'Stopped') return + await new Promise((r) => setTimeout(r, 1000)) + } + throw new Error(`Timed out waiting for ${name} to reach Stopped status`) +} + +/** Navigate to the home page and wait until all test pipelines are visible. */ +async function gotoAndWaitForPipelines(page: import('@playwright/test').Page) { + await page.goto('/') + for (const name of PIPELINES) { + await expect(page.getByTestId(`box-row-${name}`)).toBeVisible() + } +} + +test.describe('Pipeline search', () => { + test.setTimeout(180_000) + + test.beforeAll(async ({}, testInfo) => { + testInfo.setTimeout(120_000) + await cleanupPipelines() + for (const name of PIPELINES) { + await putPipeline(name, { + name, + description: `E2E search test pipeline: ${name}`, + program_code: 'create view test as (select 1)' + }) + } + // Wait for all pipelines to finish compiling via API polling, + // so individual tests don't need to wait on DOM status changes. + for (const name of PIPELINES) { + await waitForStopped(name) + } + }) + + test.afterAll(async ({}, testInfo) => { + testInfo.setTimeout(60_000) + await cleanupPipelines() + }) + + test('filters pipelines by name substring', async ({ page }) => { + await gotoAndWaitForPipelines(page) + + const searchInput = page.getByTestId('input-pipeline-search') + await searchInput.fill('alpha') + + await expect(page.getByTestId(`box-row-${PREFIX}-alpha`)).toBeVisible() + await expect(page.getByTestId(`box-row-${PREFIX}-beta`)).not.toBeVisible() + await expect(page.getByTestId(`box-row-${PREFIX}-gamma`)).not.toBeVisible() + }) + + test('search is case-insensitive', async ({ page }) => { + await gotoAndWaitForPipelines(page) + + const searchInput = page.getByTestId('input-pipeline-search') + await searchInput.fill(PREFIX.toUpperCase()) + + // All three pipelines share the prefix, so all should be visible + for (const name of PIPELINES) { + await expect(page.getByTestId(`box-row-${name}`)).toBeVisible() + } + }) + + test('shows empty state when no pipelines match', async ({ page }) => { + await gotoAndWaitForPipelines(page) + + const searchInput = page.getByTestId('input-pipeline-search') + await searchInput.fill('nonexistent-pipeline-xyz-999') + + await expect(page.getByText('No pipelines found')).toBeVisible() + }) + + test('clearing search shows all pipelines again', async ({ page }) => { + await gotoAndWaitForPipelines(page) + + const searchInput = page.getByTestId('input-pipeline-search') + await searchInput.fill('alpha') + await expect(page.getByTestId(`box-row-${PREFIX}-beta`)).not.toBeVisible() + + await searchInput.clear() + + for (const name of PIPELINES) { + await expect(page.getByTestId(`box-row-${name}`)).toBeVisible() + } + }) + + test('search works together with status filter', async ({ page }) => { + await gotoAndWaitForPipelines(page) + + // All test pipelines should be in "Stopped" (Ready To Start) status + const statusSelect = page.getByTestId('select-pipeline-status') + await statusSelect.selectOption('Ready To Start') + + const searchInput = page.getByTestId('input-pipeline-search') + await searchInput.fill('beta') + + await expect(page.getByTestId(`box-row-${PREFIX}-beta`)).toBeVisible() + await expect(page.getByTestId(`box-row-${PREFIX}-alpha`)).not.toBeVisible() + + // Switching to a non-matching status should hide everything + await statusSelect.selectOption('Running') + await expect(page.getByTestId(`box-row-${PREFIX}-beta`)).not.toBeVisible() + }) +}) From 007bb8ac84c455dcfc09f48940c2c1dc423fca9b Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Mon, 6 Apr 2026 11:01:38 -0700 Subject: [PATCH 3/4] [dbsp] Use approximate size for size of in-memory batches. When Feldera takes a profile, it measures the size of all the batches in each spine using SizeOf. This is very expensive for large batches, in some cases taking over a second of CPU time. This commit fixes the problem. Another way to fix the problem would be to calculate the exact size and save it. This could be done at batch creation time unconditionally, or the first time we measure it and cached. But this would still take the same large amount of CPU time, we'd just be able to avoid taking it more than once. And if we did it lazily and cached it, it would still take a lot of time in profiles, especially the first time a profile runs. Signed-off-by: Ben Pfaff --- .../src/trace/ord/vec/indexed_wset_batch.rs | 17 ++++++++++++++--- crates/dbsp/src/trace/ord/vec/key_batch.rs | 16 ++++++++++++++-- crates/dbsp/src/trace/ord/vec/val_batch.rs | 17 +++++++++++++++-- crates/dbsp/src/trace/ord/vec/wset_batch.rs | 14 ++++++++++++-- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/crates/dbsp/src/trace/ord/vec/indexed_wset_batch.rs b/crates/dbsp/src/trace/ord/vec/indexed_wset_batch.rs index 092e87d671..1213fb0c4e 100644 --- a/crates/dbsp/src/trace/ord/vec/indexed_wset_batch.rs +++ b/crates/dbsp/src/trace/ord/vec/indexed_wset_batch.rs @@ -158,7 +158,6 @@ where type Layers = Layer, O>; /// An immutable collection of update tuples. -#[derive(SizeOf)] pub struct VecIndexedWSet where K: DataTrait + ?Sized, @@ -169,9 +168,7 @@ where /// Where all the data is. #[doc(hidden)] pub layer: Layers, - #[size_of(skip)] factories: VecIndexedWSetFactories, - #[size_of(skip)] negative_weight_count: u64, } @@ -203,6 +200,20 @@ where } } +impl SizeOf for VecIndexedWSet +where + K: DataTrait + ?Sized, + V: DataTrait + ?Sized, + R: WeightTrait + ?Sized, + O: OrdOffset, +{ + fn size_of_children(&self, context: &mut size_of::Context) { + // This is only approximate but it is *much* cheaper than measuring all + // the elements individually. + context.add(self.approximate_byte_size()); + } +} + impl PartialEq for VecIndexedWSet where K: DataTrait + ?Sized, diff --git a/crates/dbsp/src/trace/ord/vec/key_batch.rs b/crates/dbsp/src/trace/ord/vec/key_batch.rs index a1598db2d4..af192c1714 100644 --- a/crates/dbsp/src/trace/ord/vec/key_batch.rs +++ b/crates/dbsp/src/trace/ord/vec/key_batch.rs @@ -148,7 +148,6 @@ pub type VecKeyBatchLayer = Layer, R>, O>; /// An immutable collection of update tuples, from a contiguous interval of /// logical times. -#[derive(SizeOf)] pub struct VecKeyBatch where K: DataTrait + ?Sized, @@ -158,10 +157,23 @@ where { /// Where all the dataz is. pub layer: VecKeyBatchLayer, - #[size_of(skip)] factories: VecKeyBatchFactories, } +impl SizeOf for VecKeyBatch +where + K: DataTrait + ?Sized, + R: WeightTrait + ?Sized, + T: Timestamp, + O: OrdOffset, +{ + fn size_of_children(&self, context: &mut size_of::Context) { + // This is only approximate but it is *much* cheaper than measuring all + // the elements individually. + context.add(self.approximate_byte_size()); + } +} + impl Debug for VecKeyBatch where K: DataTrait + ?Sized, diff --git a/crates/dbsp/src/trace/ord/vec/val_batch.rs b/crates/dbsp/src/trace/ord/vec/val_batch.rs index e19217ba53..c8810f2983 100644 --- a/crates/dbsp/src/trace/ord/vec/val_batch.rs +++ b/crates/dbsp/src/trace/ord/vec/val_batch.rs @@ -182,7 +182,6 @@ where /// An immutable collection of update tuples, from a contiguous interval of /// logical times. -#[derive(SizeOf)] pub struct VecValBatch where K: DataTrait + ?Sized, @@ -191,7 +190,6 @@ where T: Timestamp, O: OrdOffset, { - #[size_of(skip)] factories: VecValBatchFactories, // #[size_of(skip)] @@ -204,6 +202,21 @@ where pub layer: VecValBatchLayer, } +impl SizeOf for VecValBatch +where + K: DataTrait + ?Sized, + V: DataTrait + ?Sized, + R: WeightTrait + ?Sized, + T: Timestamp, + O: OrdOffset, +{ + fn size_of_children(&self, context: &mut size_of::Context) { + // This is only approximate but it is *much* cheaper than measuring all + // the elements individually. + context.add(self.approximate_byte_size()); + } +} + unsafe impl Send for VecValBatch where K: DataTrait + ?Sized, diff --git a/crates/dbsp/src/trace/ord/vec/wset_batch.rs b/crates/dbsp/src/trace/ord/vec/wset_batch.rs index 2e262d21ed..4a5b4050b8 100644 --- a/crates/dbsp/src/trace/ord/vec/wset_batch.rs +++ b/crates/dbsp/src/trace/ord/vec/wset_batch.rs @@ -113,7 +113,6 @@ impl BatchFactories where K: DataTrait + ?Sized, @@ -121,11 +120,22 @@ where { #[doc(hidden)] pub layer: Leaf, - #[size_of(skip)] factories: VecWSetFactories, negative_weight_count: u64, } +impl SizeOf for VecWSet +where + K: DataTrait + ?Sized, + R: WeightTrait + ?Sized, +{ + fn size_of_children(&self, context: &mut size_of::Context) { + // This is only approximate but it is *much* cheaper than measuring all + // the elements individually. + context.add(self.approximate_byte_size()); + } +} + impl VecWSet where K: DataTrait + ?Sized, From 6b74dab3662bea545868fac3101cfdd9a581c567 Mon Sep 17 00:00:00 2001 From: Karakatiza666 Date: Mon, 6 Apr 2026 11:38:25 +0000 Subject: [PATCH 4/4] [web-console] Make pipelines table header sticky Signed-off-by: Karakatiza666 --- .../lib/components/layout/AppHeader.svelte | 2 +- .../src/lib/components/pipelines/Table.svelte | 345 ++++++++++-------- .../pipelines/list/PipelineStatus.svelte | 2 +- .../(system)/(authenticated)/+layout.svelte | 2 +- .../(system)/(authenticated)/+page.svelte | 198 +++++----- .../web-console/tests/pipelineSearch.e2e.ts | 6 +- 6 files changed, 297 insertions(+), 258 deletions(-) diff --git a/js-packages/web-console/src/lib/components/layout/AppHeader.svelte b/js-packages/web-console/src/lib/components/layout/AppHeader.svelte index 31c1bc6de7..3a8c01c408 100644 --- a/js-packages/web-console/src/lib/components/layout/AppHeader.svelte +++ b/js-packages/web-console/src/lib/components/layout/AppHeader.svelte @@ -15,7 +15,7 @@ const healthStatus = useClusterHealth() -
No pipelines with the specified statusNo pipelines found
- - - - Pipeline name - - Status - - p.connectors?.numErrors} +
+
+
+ {#if header} + {@render header()} + {/if} +
+ { + nameSearch = e.currentTarget.value + }} + /> +
- - - {#each table.rows as pipeline} - + {#each table.rows as pipeline} + + + + + + + + + + + {:else} + + + + + {/each} + +
table.selectAll()} - />StorageMessage
- {filter[0]} + {/each} + + {@render preHeaderEnd?.()} + + + + + + + + table.selectAll()} + /> - - Pipeline name - + p.connectors?.numErrors} + > + + Errors + - - - - - - - {:else} - - - + + + + Runtime + + + Status changed + Deployed on - {/each} - -
table.select(pipeline.name)} - /> - - {pipeline.name} -
- {match(pipeline.storageStatus) - .with('InUse', () => 'Storage in use') - .with('Clearing', () => 'Clearing storage') - .with('Cleared', () => 'Storage cleared') - .exhaustive()} -
Storage - - {#if pipeline.deploymentError} - {@const message = pipeline.deploymentError.message} - - -
- {message} -
-
- {message.slice(0, ((idx) => (idx > 0 ? idx : undefined))(message.indexOf('\n')))} - {/if} + Status +
Message -
- {pipeline.connectors?.numErrors ?? '-'} -
-
-
- -
-
-
- {formatElapsedTime(pipeline.lastStatusSince, 'dhm')} ago -
-
-
- {pipeline.deploymentResourcesStatus === 'Provisioned' - ? formatDateTime(pipeline.deploymentResourcesStatusSince) - : ''} -
-
No pipelines found
-
+ +
+ table.select(pipeline.name)} + /> + {pipeline.name} +
+ {match(pipeline.storageStatus) + .with('InUse', () => 'Storage in use') + .with('Clearing', () => 'Clearing storage') + .with('Cleared', () => 'Storage cleared') + .exhaustive()} +
+ + {#if pipeline.deploymentError} + {@const message = pipeline.deploymentError.message} + + +
+ {message} +
+
+ {message.slice(0, ((idx) => (idx > 0 ? idx : undefined))(message.indexOf('\n')))} + {/if} +
+
+
+ {pipeline.connectors?.numErrors ?? '-'} +
+
+
+ +
+
+
+ {formatElapsedTime(pipeline.lastStatusSince, 'dhm')} ago +
+
+
+ {pipeline.deploymentResourcesStatus === 'Provisioned' + ? formatDateTime(pipeline.deploymentResourcesStatusSince) + : ''} +
+
No pipelines found
+
+
+ + diff --git a/js-packages/web-console/src/lib/components/pipelines/list/PipelineStatus.svelte b/js-packages/web-console/src/lib/components/pipelines/list/PipelineStatus.svelte index 224f7ac7ff..21c26dfd29 100644 --- a/js-packages/web-console/src/lib/components/pipelines/list/PipelineStatus.svelte +++ b/js-packages/web-console/src/lib/components/pipelines/list/PipelineStatus.svelte @@ -7,6 +7,6 @@ const chipClass = $derived(pipelineStatusColor(status).chip) -
+
{getPipelineStatusLabel(status)}
diff --git a/js-packages/web-console/src/routes/(system)/(authenticated)/+layout.svelte b/js-packages/web-console/src/routes/(system)/(authenticated)/+layout.svelte index 068d33680a..d39600e24f 100644 --- a/js-packages/web-console/src/routes/(system)/(authenticated)/+layout.svelte +++ b/js-packages/web-console/src/routes/(system)/(authenticated)/+layout.svelte @@ -120,7 +120,7 @@ ignoreAfterNavigate={() => false} >
-
- {#if !welcomed.value} -
-
-
-
+
+
+ {#if !welcomed.value} +
+
+
+
+
+
+ {#if darkMode.current === 'dark'} + + {:else} + + {/if} +
+
+
Explore our communities and documentation
+ +
+ +
+ {#each featured as link} + {link.title} + {/each} +
+
+
- {#if darkMode.current === 'dark'} - + {/if} +
+ {#if pipelines.pipelines.length} + + {#snippet header()} +
+ Your pipelines +
+ {/snippet} + {#snippet preHeaderEnd()} + + {#if !selectedPipelines.length} + + {/if} + {/snippet} +
{:else} - - {/if} -
- - {/if} -
-
- Your pipelines + {/if}
- {#if pipelines.pipelines.length} - - {#snippet preHeaderEnd()} - - {#if !selectedPipelines.length} - - {/if} - {/snippet} - - {:else} -
- -
Your pipelines will appear here
- -
- {/if} -
- {#if data.demos.length} -
- - {#snippet header(open, toggle)} - - {/snippet} - -
- {/if} + {/snippet} + +
+ {/if} +
-
+
diff --git a/js-packages/web-console/tests/pipelineSearch.e2e.ts b/js-packages/web-console/tests/pipelineSearch.e2e.ts index 4be830a9a2..c60377e847 100644 --- a/js-packages/web-console/tests/pipelineSearch.e2e.ts +++ b/js-packages/web-console/tests/pipelineSearch.e2e.ts @@ -1,10 +1,6 @@ import { expect, test } from '@playwright/test' import { client } from '$lib/services/manager/client.gen' -import { - deletePipeline, - getExtendedPipeline, - putPipeline -} from '$lib/services/pipelineManager' +import { deletePipeline, getExtendedPipeline, putPipeline } from '$lib/services/pipelineManager' const API_ORIGIN = (process.env.PLAYWRIGHT_API_ORIGIN ?? 'http://localhost:8080').replace(/\/$/, '') client.setConfig({ baseUrl: API_ORIGIN })