diff --git a/Cargo.lock b/Cargo.lock index 179949b262e..ab34665f410 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 10d170e2164..6e219441602 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/crates/dbsp/src/trace/ord/vec/indexed_wset_batch.rs b/crates/dbsp/src/trace/ord/vec/indexed_wset_batch.rs index 092e87d6717..1213fb0c4e5 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 a1598db2d41..af192c1714a 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 e19217ba530..c8810f2983d 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 2e262d21ede..4a5b4050b85 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, 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 31c1bc6de75..3a8c01c4081 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() -
+
+ + 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 224f7ac7ffe..21c26dfd296 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/lib/components/pipelines/table/AvailableActions.svelte b/js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte index 09c686c8427..f1ed86c79a9 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 30815474e7e..984922c0b8e 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/src/routes/(system)/(authenticated)/+layout.svelte b/js-packages/web-console/src/routes/(system)/(authenticated)/+layout.svelte index 068d33680af..d39600e24f5 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 new file mode 100644 index 00000000000..c60377e8473 --- /dev/null +++ b/js-packages/web-console/tests/pipelineSearch.e2e.ts @@ -0,0 +1,128 @@ +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() + }) +}) diff --git a/openapi.json b/openapi.json index 3b951bec1a4..9be5a505c97 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 6d2d94693f4..2877cbf7397 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 036a757848d..c0f036e7d88 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'" },