diff --git a/backend/windmill-api-assets/src/lib.rs b/backend/windmill-api-assets/src/lib.rs index d1dfde41e0..0c25cd243d 100644 --- a/backend/windmill-api-assets/src/lib.rs +++ b/backend/windmill-api-assets/src/lib.rs @@ -10,6 +10,7 @@ use windmill_common::{ assets::{AssetKind, AssetUsageKind}, db::UserDB, error::JsonResult, + utils::escape_ilike_pattern, }; use windmill_api_auth::ApiAuthed; @@ -34,6 +35,7 @@ struct ListAssetsQuery { pub path: Option, // Filter by matching a subset of the columns using base64 encoded json subset pub columns: Option, + pub broad_filter: Option, } fn default_per_page() -> i64 { @@ -128,6 +130,14 @@ async fn list_assets( asset_summary_filters.push(format!("asset.kind = ANY(${})", param_count)); } + if query.broad_filter.is_some() { + param_count += 1; + asset_summary_filters.push(format!( + "(asset.path ILIKE ${p} OR asset.kind::text ILIKE ${p})", + p = param_count + )); + } + let asset_summary_where = asset_summary_filters.join(" AND "); // Build cursor condition @@ -224,7 +234,7 @@ async fn list_assets( let mut query_builder = sqlx::query(&sql).bind(&w_id).bind(limit); if let Some(ref asset_path) = query.asset_path { - query_builder = query_builder.bind(format!("%{}%", asset_path)); + query_builder = query_builder.bind(format!("%{}%", escape_ilike_pattern(asset_path))); } if let Some(ref path) = query.path { @@ -242,7 +252,7 @@ async fn list_assets( } if let Some(ref usage_path) = query.usage_path { - query_builder = query_builder.bind(format!("%{}%", usage_path)); + query_builder = query_builder.bind(format!("%{}%", escape_ilike_pattern(usage_path))); } if let Some(ref asset_kinds) = asset_kinds { @@ -251,6 +261,10 @@ async fn list_assets( } } + if let Some(ref broad_filter) = query.broad_filter { + query_builder = query_builder.bind(format!("%{}%", escape_ilike_pattern(broad_filter))); + } + if let (Some(cursor_created_at), Some(cursor_id)) = (query.cursor_created_at, query.cursor_id) { query_builder = query_builder.bind(cursor_created_at).bind(cursor_id); } diff --git a/backend/windmill-api-jobs/src/concurrency_groups.rs b/backend/windmill-api-jobs/src/concurrency_groups.rs index a727b7b02c..33f6045e49 100644 --- a/backend/windmill-api-jobs/src/concurrency_groups.rs +++ b/backend/windmill-api-jobs/src/concurrency_groups.rs @@ -225,6 +225,7 @@ async fn get_concurrent_intervals( allow_wildcards: None, trigger_kind: _, include_args: _, + broad_filter: _, } => true, _ => false, }; diff --git a/backend/windmill-api-jobs/src/query.rs b/backend/windmill-api-jobs/src/query.rs index d3cc87fa07..6be9487191 100644 --- a/backend/windmill-api-jobs/src/query.rs +++ b/backend/windmill-api-jobs/src/query.rs @@ -10,7 +10,7 @@ use sql_builder::prelude::*; use sql_builder::SqlBuilder; -use windmill_common::utils::{paginate_without_limits, Pagination}; +use windmill_common::utils::{escape_ilike_pattern, paginate_without_limits, Pagination}; use crate::types::{ListCompletedQuery, ListQueueQuery}; @@ -229,6 +229,14 @@ pub fn filter_list_queue_query( } } + if let Some(bf) = &lq.broad_filter { + let pat = format!("%{}%", escape_ilike_pattern(bf)); + sqlb.and_where( + "(runnable_path ILIKE ? OR v2_job.tag ILIKE ? OR trigger ILIKE ? OR trigger_kind::text ILIKE ?)" + .bind(&pat).bind(&pat).bind(&pat).bind(&pat) + ); + } + sqlb } @@ -524,6 +532,15 @@ pub fn filter_list_completed_query( } } + if let Some(bf) = &lq.broad_filter { + let pat = format!("%{}%", escape_ilike_pattern(bf)); + sqlb.and_where( + "(runnable_path ILIKE ? OR v2_job.tag ILIKE ? OR trigger ILIKE ? OR trigger_kind::text ILIKE ? \ + OR EXISTS (SELECT 1 FROM jsonb_array_elements_text(result->'wm_labels') lbl WHERE jsonb_typeof(result->'wm_labels') = 'array' AND lbl ILIKE ?))" + .bind(&pat).bind(&pat).bind(&pat).bind(&pat).bind(&pat) + ); + } + sqlb } @@ -598,6 +615,7 @@ mod tests { trigger_kind: None, trigger_path: None, include_args: None, + broad_filter: None, } } @@ -641,6 +659,7 @@ mod tests { trigger_kind: None, trigger_path: None, include_args: None, + broad_filter: None, } } diff --git a/backend/windmill-api-jobs/src/types.rs b/backend/windmill-api-jobs/src/types.rs index 33f312e0b4..c59a54bd1c 100644 --- a/backend/windmill-api-jobs/src/types.rs +++ b/backend/windmill-api-jobs/src/types.rs @@ -121,6 +121,7 @@ pub struct ListQueueQuery { pub trigger_kind: Option>, pub trigger_path: Option>, pub include_args: Option, + pub broad_filter: Option, } #[derive(Deserialize, Clone)] @@ -165,6 +166,7 @@ pub struct ListCompletedQuery { pub trigger_kind: Option>, pub trigger_path: Option>, pub include_args: Option, + pub broad_filter: Option, } impl From for ListQueueQuery { @@ -199,6 +201,7 @@ impl From for ListQueueQuery { trigger_kind: lcq.trigger_kind, trigger_path: lcq.trigger_path, include_args: lcq.include_args, + broad_filter: lcq.broad_filter, } } } @@ -703,6 +706,7 @@ mod tests { trigger_kind: None, trigger_path: None, include_args: None, + broad_filter: None, }; let lqq: ListQueueQuery = lcq.into(); @@ -770,6 +774,7 @@ mod tests { trigger_kind: None, trigger_path: None, include_args: None, + broad_filter: None, }; let lqq: ListQueueQuery = lcq.into(); diff --git a/backend/windmill-api-schedule/src/lib.rs b/backend/windmill-api-schedule/src/lib.rs index 641fbfee56..7a82523ff9 100644 --- a/backend/windmill-api-schedule/src/lib.rs +++ b/backend/windmill-api-schedule/src/lib.rs @@ -25,7 +25,9 @@ use windmill_common::{ db::UserDB, error::{Error, JsonResult, Result}, schedule::Schedule, - utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath}, + utils::{ + escape_ilike_pattern, not_found_if_none, paginate, Pagination, ScheduleType, StripPath, + }, worker::to_raw_value, }; use windmill_git_sync::{handle_deployment_metadata, DeployedObject}; @@ -592,6 +594,7 @@ pub struct ListScheduleQuery { pub description: Option, // filter on summary (pattern match) pub summary: Option, + pub broad_filter: Option, } #[derive(sqlx::FromRow, Serialize, Deserialize, Debug, Clone)] @@ -651,13 +654,19 @@ async fn list_schedule( sqlb.and_where_eq("path", "?".bind(schedule_path)); } if let Some(description) = &lsq.description { - sqlb.and_where(&format!( - "description ILIKE '%{}%'", - description.replace("'", "''") - )); + let pat = format!("%{}%", escape_ilike_pattern(description)); + sqlb.and_where("description ILIKE ?".bind(&pat)); } if let Some(summary) = &lsq.summary { - sqlb.and_where(&format!("summary ILIKE '%{}%'", summary.replace("'", "''"))); + let pat = format!("%{}%", escape_ilike_pattern(summary)); + sqlb.and_where("summary ILIKE ?".bind(&pat)); + } + if let Some(broad_filter) = &lsq.broad_filter { + let pat = format!("%{}%", escape_ilike_pattern(broad_filter)); + sqlb.and_where( + "(path ILIKE ? OR script_path ILIKE ? OR description ILIKE ? OR summary ILIKE ? OR schedule ILIKE ?)" + .bind(&pat).bind(&pat).bind(&pat).bind(&pat).bind(&pat) + ); } let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?; let rows = sqlx::query_as::<_, ScheduleLight>(&sql) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index cd78165aa9..d93953abda 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -4106,6 +4106,11 @@ paths: in: query schema: type: string + - name: broad_filter + description: broad search across multiple fields (case-insensitive substring match) + in: query + schema: + type: string - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PerPage" responses: @@ -5120,6 +5125,11 @@ paths: in: query schema: type: string + - name: broad_filter + description: broad search across multiple fields (case-insensitive substring match) + in: query + schema: + type: string responses: "200": description: resource list @@ -9996,6 +10006,11 @@ paths: in: query schema: type: boolean + - name: broad_filter + description: broad search across multiple fields (case-insensitive substring match on path, tag, schedule path, trigger kind, label) + in: query + schema: + type: string responses: "200": description: All jobs @@ -11220,6 +11235,11 @@ paths: in: query schema: type: string + - name: broad_filter + description: broad search across multiple fields (case-insensitive substring match) + in: query + schema: + type: string responses: "200": description: schedule list @@ -16981,6 +17001,11 @@ paths: description: JSONB subset match filter for columns using base64 encoded JSON schema: type: string + - name: broad_filter + description: broad search across multiple fields (case-insensitive substring match) + in: query + schema: + type: string responses: "200": description: paginated assets in the workspace diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index 2fec2f9e94..e4c5315cba 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -202,6 +202,15 @@ impl StripPath { } } +/// Escape ILIKE special characters (`%`, `_`, `\`) so user input is matched +/// literally. Use this when building `ILIKE '%…%'` patterns from user-supplied +/// strings to prevent wildcard injection. +pub fn escape_ilike_pattern(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('%', "\\%") + .replace('_', "\\_") +} + pub fn require_admin(is_admin: bool, username: &str) -> Result<()> { if !is_admin { Err(Error::RequireAdmin(username.to_string())) diff --git a/backend/windmill-store/src/resources.rs b/backend/windmill-store/src/resources.rs index 269edf2612..bcac53e7d2 100644 --- a/backend/windmill-store/src/resources.rs +++ b/backend/windmill-store/src/resources.rs @@ -17,7 +17,7 @@ use windmill_common::workspaces::{check_user_against_rule, ProtectionRuleKind, R use crate::secret_backend_ext::rename_vault_secret; use crate::var_resource_cache::{cache_resource, get_cached_resource}; -use windmill_common::utils::BulkDeleteRequest; +use windmill_common::utils::{escape_ilike_pattern, BulkDeleteRequest}; use windmill_common::webhook::{WebhookMessage, WebhookShared}; use axum::{ @@ -170,6 +170,7 @@ pub struct ListResourceQuery { pub description: Option, // filter by matching a subset of the value using base64 encoded json subset pub value: Option, + pub broad_filter: Option, } #[derive(Serialize, FromRow)] @@ -294,13 +295,22 @@ async fn list_resources( } if let Some(description) = &lq.description { - sqlb.and_where("resource.description ILIKE ?".bind(&format!("%{}%", description))); + let pat = format!("%{}%", escape_ilike_pattern(description)); + sqlb.and_where("resource.description ILIKE ?".bind(&pat)); } if let Some(value) = &lq.value { sqlb.and_where("resource.value @> ?".bind(&value.replace("'", "''"))); } + if let Some(broad_filter) = &lq.broad_filter { + let pat = format!("%{}%", escape_ilike_pattern(broad_filter)); + sqlb.and_where( + "(resource.path ILIKE ? OR resource.description ILIKE ? OR resource_type ILIKE ? OR resource.value::text ILIKE ?)" + .bind(&pat).bind(&pat).bind(&pat).bind(&pat) + ); + } + let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?; let mut tx = user_db.begin(&authed).await?; let rows = sqlx::query_as::<_, ListableResource>(&sql) diff --git a/backend/windmill-store/src/variables.rs b/backend/windmill-store/src/variables.rs index 0a6ecb643a..233115896f 100644 --- a/backend/windmill-store/src/variables.rs +++ b/backend/windmill-store/src/variables.rs @@ -14,7 +14,7 @@ use crate::secret_backend_ext::{ delete_secret_from_backend, get_secret_value, is_vault_stored_value, rename_vault_secret, store_secret_value, }; -use windmill_common::utils::BulkDeleteRequest; +use windmill_common::utils::{escape_ilike_pattern, BulkDeleteRequest}; use windmill_common::webhook::{WebhookMessage, WebhookShared}; use axum::{ @@ -101,6 +101,7 @@ struct ListVariableQuery { pub description: Option, // filter by matching the non-encrypted value (for non-secrets only) pub value: Option, + pub broad_filter: Option, } async fn list_variables( @@ -161,18 +162,22 @@ async fn list_variables( } if let Some(description) = &lq.description { - sqlb.and_where(&format!( - "variable.description ILIKE '%{}%'", - description.replace("'", "''") - )); + let pat = format!("%{}%", escape_ilike_pattern(description)); + sqlb.and_where("variable.description ILIKE ?".bind(&pat)); } if let Some(value) = &lq.value { // Only filter on non-secret variables' value - sqlb.and_where(&format!( - "(is_secret = FALSE AND variable.value ILIKE '%{}%')", - value.replace("'", "''") - )); + let pat = format!("%{}%", escape_ilike_pattern(value)); + sqlb.and_where("(is_secret = FALSE AND variable.value ILIKE ?)".bind(&pat)); + } + + if let Some(broad_filter) = &lq.broad_filter { + let pat = format!("%{}%", escape_ilike_pattern(broad_filter)); + sqlb.and_where( + "(variable.path ILIKE ? OR variable.description ILIKE ? OR (is_secret = FALSE AND variable.value ILIKE ?))" + .bind(&pat).bind(&pat).bind(&pat) + ); } let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?; diff --git a/frontend/src/lib/components/FilterSearchbar.svelte b/frontend/src/lib/components/FilterSearchbar.svelte index 1b7f426d98..8b902a8896 100644 --- a/frontend/src/lib/components/FilterSearchbar.svelte +++ b/frontend/src/lib/components/FilterSearchbar.svelte @@ -27,6 +27,7 @@ label?: string description?: string icon?: IconType + hidden?: boolean } export type FilterInstanceRec = { [K in keyof T]: FilterInstance @@ -289,14 +290,24 @@ let taggedTextInput: TaggedTextInput | undefined = $state() let tags = $derived( - Object.entries(schema).map(([key, filterSchema]) => ({ - regex: new RegExp(`\\b${key}:(?:\\\\.|[^\\s])*`, 'g'), - id: key, - onClear: () => (delete value[key], asText.reparse()) - })) + Object.entries(schema) + .filter(([_, s]) => !s.hidden) + .map(([key, filterSchema]) => ({ + regex: new RegExp(`\\b${key}:(?:\\\\.|[^\\s])*`, 'g'), + id: key, + onClear: () => (delete value[key], asText.reparse()) + })) ) - let keyHighlightRegex = $derived(new RegExp(`\\b(${Object.keys(schema).join('|')}):`, 'g')) + let keyHighlightRegex = $derived( + new RegExp( + `\\b(${Object.entries(schema) + .filter(([_, s]) => !s.hidden) + .map(([k]) => k) + .join('|')}):`, + 'g' + ) + ) let errorKeys = $derived(new Set(errors.flatMap((e) => e.fields))) let errorHighlights = $derived( @@ -310,6 +321,7 @@ if (!currentTag) { const searchText = currentTextSegment.text.trim().toLowerCase() return Object.entries(schema) + .filter(([_, s]) => !s.hidden) .filter(([k, _]) => !(k in value)) .filter(([k, filterSchema]) => { if (!searchText) return true @@ -397,34 +409,61 @@ function parseFromText(text: string): Partial> { const parsed: Record = {} + const matchedRanges: [number, number][] = [] let match while ((match = kvRegex.exec(text)) !== null) { - let [_, key, val] = match - if (key in schema) { + let [fullMatch, key, val] = match + if (key in schema && key !== '_default_') { val ??= '' val = val.replace(/\\(.)/g, (_: string, c: string) => { if (c === 'n') return '\n' if (c === 'r') return '\r' return c - }) // Unescape escaped characters + }) val = val.trim() parsed[key] = textToFilter(val, schema[key]) as any + matchedRanges.push([match.index, match.index + fullMatch.length]) } } + + if ('_default_' in schema) { + let remaining = '' + let lastEnd = 0 + matchedRanges.sort((a, b) => a[0] - b[0]) + for (const [start, end] of matchedRanges) { + remaining += text.slice(lastEnd, start) + lastEnd = end + } + remaining += text.slice(lastEnd) + remaining = remaining.replace(/\s+/g, ' ').trim() + if (remaining) { + parsed['_default_'] = remaining + } + } + return parsed } function parseToText(v: Partial>): string { - return ( - Object.entries(v) - .map(([key, val]) => - `${key}: ${filterToText(val as any, schema[key])}` - .replace(/ /g, '\\ ') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - ) - .join(' ') + '\u00A0' - ) + const parts: string[] = [] + let defaultText = '' + for (const [key, val] of Object.entries(v)) { + if (key === '_default_') { + defaultText = String(val) + continue + } + parts.push( + `${key}: ${filterToText(val as any, schema[key])}` + .replace(/ /g, '\\ ') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + ) + } + let result = parts.join(' ') + if (defaultText) { + result = result ? `${result} ${defaultText}` : defaultText + } + return result + '\u00A0' } let asText = useTransformedSyncedValue( diff --git a/frontend/src/lib/components/RunsPage.svelte b/frontend/src/lib/components/RunsPage.svelte index 6682ab58d3..635a805511 100644 --- a/frontend/src/lib/components/RunsPage.svelte +++ b/frontend/src/lib/components/RunsPage.svelte @@ -718,7 +718,11 @@ bind:value={_timeframe.val} /> void } - let { loading, size = 'md', onClick }: Props = $props() + let { loading, size = 'md', onClick, variant = 'subtle' }: Props = $props() let buttonHover = $state(false) @@ -22,7 +22,8 @@ on:mouseleave={() => (buttonHover = false)} color="light" unifiedSize={size} - variant="subtle" + {variant} + iconOnly {onClick} startIcon={{ icon: RefreshCw, props: { class: loading ? 'animate-spin' : '' } }} > diff --git a/frontend/src/lib/components/flows/map/InsertModuleInner.svelte b/frontend/src/lib/components/flows/map/InsertModuleInner.svelte index e3f0c3824a..71d87b5a35 100644 --- a/frontend/src/lib/components/flows/map/InsertModuleInner.svelte +++ b/frontend/src/lib/components/flows/map/InsertModuleInner.svelte @@ -69,7 +69,6 @@ {/if} { refreshCount.val += 1 diff --git a/frontend/src/lib/components/resources/resourcesFilter.ts b/frontend/src/lib/components/resources/resourcesFilter.ts index a95851fde0..555a90f01c 100644 --- a/frontend/src/lib/components/resources/resourcesFilter.ts +++ b/frontend/src/lib/components/resources/resourcesFilter.ts @@ -15,6 +15,10 @@ export function buildResourcesFilterSchema({ userFoldersLabel?: string }) { return { + _default_: { + type: 'string' as const, + hidden: true + }, resource_type: { type: 'oneof' as const, options: resourceTypes.map((s) => ({ label: s, value: s })), diff --git a/frontend/src/lib/components/runs/runsFilter.ts b/frontend/src/lib/components/runs/runsFilter.ts index ace021abed..289ed66535 100644 --- a/frontend/src/lib/components/runs/runsFilter.ts +++ b/frontend/src/lib/components/runs/runsFilter.ts @@ -31,6 +31,10 @@ export function buildRunsFilterSearchbarSchema({ isSuperAdmin: boolean }) { return { + _default_: { + type: 'string' as const, + hidden: true + }, min_ts: { type: 'date' as const, label: 'From', diff --git a/frontend/src/lib/components/runs/useJobsLoader.svelte.ts b/frontend/src/lib/components/runs/useJobsLoader.svelte.ts index d853149fd9..384e925564 100644 --- a/frontend/src/lib/components/runs/useJobsLoader.svelte.ts +++ b/frontend/src/lib/components/runs/useJobsLoader.svelte.ts @@ -95,6 +95,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { let folder = $derived(filters?.folder) let path = $derived(filters?.path) let argFilter = $derived(filters?.arg) + let broadFilter = $derived(filters?._default_ || undefined) let queue_count: Tweened | undefined = $state() let suspended_count: Tweened | undefined = $state() @@ -217,7 +218,8 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { triggerKind: jobTriggerKind ?? undefined, allWorkspaces: allWorkspaces ? true : undefined, perPage, - allowWildcards: allowWildcards ? true : undefined + allowWildcards: allowWildcards ? true : undefined, + broadFilter }) promise = CancelablePromiseUtils.catchErr(promise, (e) => { if (e instanceof CancelError) return CancelablePromiseUtils.err(e) diff --git a/frontend/src/lib/components/schedules/schedulesFilter.ts b/frontend/src/lib/components/schedules/schedulesFilter.ts index 746c34777e..4e96278786 100644 --- a/frontend/src/lib/components/schedules/schedulesFilter.ts +++ b/frontend/src/lib/components/schedules/schedulesFilter.ts @@ -13,6 +13,10 @@ export function buildSchedulesFilterSchema({ userFoldersLabel?: string }) { return { + _default_: { + type: 'string' as const, + hidden: true + }, schedule_path: { type: 'oneof' as const, options: paths.map((s) => ({ label: s, value: s })), diff --git a/frontend/src/lib/components/variables/variablesFilter.ts b/frontend/src/lib/components/variables/variablesFilter.ts index 1b6235fe7f..bb198bcbc7 100644 --- a/frontend/src/lib/components/variables/variablesFilter.ts +++ b/frontend/src/lib/components/variables/variablesFilter.ts @@ -13,6 +13,10 @@ export function buildVariablesFilterSchema({ userFoldersLabel?: string }) { return { + _default_: { + type: 'string' as const, + hidden: true + }, owner: { type: 'oneof' as const, options: owners.map((s) => ({ label: s, value: s })), diff --git a/frontend/src/routes/(root)/(logged)/assets/+page.svelte b/frontend/src/routes/(root)/(logged)/assets/+page.svelte index 6a8c261b14..7eac2cc535 100644 --- a/frontend/src/routes/(root)/(logged)/assets/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/assets/+page.svelte @@ -22,7 +22,7 @@ import { Tooltip } from '$lib/components/meltComponents' import { AlertTriangle, Loader2, SettingsIcon, StarIcon } from 'lucide-svelte' import { StaleWhileLoading, useInfiniteQuery, useScrollToBottom } from '$lib/svelte5Utils.svelte' - import { Debounced, resource, watch, type ResourceReturn } from 'runed' + import { resource, watch, type ResourceReturn } from 'runed' import RefreshButton from '$lib/components/common/button/RefreshButton.svelte' import Section from '$lib/components/Section.svelte' import Button from '$lib/components/common/button/Button.svelte' @@ -57,16 +57,14 @@ ) let filterValues = useUrlSyncedFilterInstance(untrack(() => assetsFilterSchema)) - let filters = new Debounced( - () => ({ - assetPath: filterValues.val.asset_path || undefined, - usagePath: filterValues.val.usage_path || undefined, - assetKinds: filterValues.val.asset_kinds || undefined, - path: filterValues.val.path || undefined, - columns: filterValues.val.columns || undefined - }), - 500 - ) + let filters = $derived.by(() => ({ + assetPath: filterValues.val.asset_path || undefined, + usagePath: filterValues.val.usage_path || undefined, + assetKinds: filterValues.val.asset_kinds || undefined, + path: filterValues.val.path || undefined, + columns: filterValues.val.columns || undefined, + broadFilter: filterValues.val._default_ || undefined + })) const assetsQuery = useInfiniteQuery({ queryFn: async (cursor) => { @@ -75,7 +73,7 @@ perPage: 50, cursorCreatedAt: cursor?.created_at, cursorId: cursor?.id, - ...filters.current + ...filters }) }, initialPageParam: undefined, @@ -91,7 +89,7 @@ ) watch( - () => [filters.current, $workspaceStore], + () => [filters, $workspaceStore], () => assetsQuery.reset() ) @@ -272,14 +270,20 @@
{#snippet action()} - assetsQuery.reset()} loading={assetsQuery.isLoading} /> +
+ assetsQuery.reset()} + loading={assetsQuery.isLoading} + /> + +
{/snippet} - {@render table()} {#if assetsQuery.isFetchingNextPage} diff --git a/frontend/src/routes/(root)/(logged)/resources/+page.svelte b/frontend/src/routes/(root)/(logged)/resources/+page.svelte index d379910b43..f0ac8ae9d6 100644 --- a/frontend/src/routes/(root)/(logged)/resources/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/resources/+page.svelte @@ -198,6 +198,9 @@ if (currentFilters.owner) { apiParams.pathStart = currentFilters.owner } + if (currentFilters._default_) { + apiParams.broadFilter = currentFilters._default_ + } const result = (await ResourceService.listResource(apiParams)).map((x) => { return { @@ -555,6 +558,10 @@ }) let dbManagerDrawer = $derived(globalDbManagerDrawer.val) as any + + let showTable = $derived( + tab == 'workspace' || tab == 'states' || tab == 'cache' || tab == 'theme' + ) -
+
- {#if tab == 'workspace' || tab == 'states' || tab == 'cache' || tab == 'theme'} - - + {#if showTable}
{#if loading.resources} diff --git a/frontend/src/routes/(root)/(logged)/schedules/+page.svelte b/frontend/src/routes/(root)/(logged)/schedules/+page.svelte index 94ef6b5cc4..04f494c629 100644 --- a/frontend/src/routes/(root)/(logged)/schedules/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/schedules/+page.svelte @@ -31,7 +31,9 @@ } from 'lucide-svelte' import { goto } from '$lib/navigation' import { sendUserToast } from '$lib/toast' - import FilterSearchbar, { useUrlSyncedFilterInstance } from '$lib/components/FilterSearchbar.svelte' + import FilterSearchbar, { + useUrlSyncedFilterInstance + } from '$lib/components/FilterSearchbar.svelte' import { buildSchedulesFilterSchema } from '$lib/components/schedules/schedulesFilter' import NoItemFound from '$lib/components/home/NoItemFound.svelte' import RowIcon from '$lib/components/common/table/RowIcon.svelte' @@ -88,6 +90,9 @@ if (currentFilters.args) { apiParams.args = currentFilters.args } + if (currentFilters._default_) { + apiParams.broadFilter = currentFilters._default_ + } const result = (await ScheduleService.listSchedules(apiParams)).map((x) => { return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x } @@ -171,9 +176,7 @@ scriptPaths: allScriptPaths, showUserFoldersFilter: userFoldersFilterType !== undefined, userFoldersLabel: - userFoldersFilterType === 'only f/*' - ? 'Only f/*' - : `Only u/${$userStore?.username} and f/*` + userFoldersFilterType === 'only f/*' ? 'Only f/*' : `Only u/${$userStore?.username} and f/*` }) ) let filters = useUrlSyncedFilterInstance(untrack(() => schedulesFilterSchema)) @@ -260,18 +263,19 @@
-
- - -
- - {#snippet children({ item })} - - - - {/snippet} - -
+
+ + {#snippet children({ item })} + + + + {/snippet} + +
{#if loading} {#each new Array(6) as _} diff --git a/frontend/src/routes/(root)/(logged)/variables/+page.svelte b/frontend/src/routes/(root)/(logged)/variables/+page.svelte index ab5e4fc23b..4082e5f8ff 100644 --- a/frontend/src/routes/(root)/(logged)/variables/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/variables/+page.svelte @@ -118,6 +118,9 @@ if (currentFilters.owner) { apiParams.pathStart = currentFilters.owner } + if (currentFilters._default_) { + apiParams.broadFilter = currentFilters._default_ + } const result = (await VariableService.listVariable(apiParams)).map((x) => { return { @@ -253,33 +256,37 @@ }} /> - - - - {#snippet extra()} - - Contextual variables are passed as environment variables when running a script and - depends on the execution context. - - {/snippet} - - +
+ + + + {#snippet extra()} + + Contextual variables are passed as environment variables when running a script and + depends on the execution context. + + {/snippet} + + + {#if tab == 'workspace'} + + {/if} +
{#if tab == 'workspace'} - -
+
{#if !filteredItems} {#each new Array(3) as _}