Files
windmill/backend/windmill-api/src/path_autocomplete.rs
T
Diego Imbert 9c28bbfd69 feat(frontend): new path component (#9017)
* stash

* ui nits

* Fix contenteditable feedback look (duplicate typing)

* fix right icon wrong position with placeholder

* user editor in Path editor takes correct width

* nits

* nit

* chore: remove assets-operator changes (moved to separate PR)

These files were mistakenly included in this PR and belong in a dedicated PR
("Allow assets page to operators").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: remove sidebar assets-operator change (moved to separate PR)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix disabled

* border nit

* Fix disabled styling

* Apply suggestion from @cubic-dev-ai[bot]

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* nit

* Update frontend/src/lib/components/text_input/TextInput.svelte

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Fix disabled tabindex and aria-disabled on contenteditable Select

The useContentEditable branch had an unconditional tabindex="0", keeping
a disabled Select in the tab order, and was missing aria-disabled.
Mirror the TextInput div branch.

Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>

* fix: drop obsolete hideFullPath prop from EditorHeader Path usage

* invalidate autocomplete paths on deploy

* nit pixel

* use Badge in auto complete

* nit prevent default

* fix(autocomplete): don't let stale fetch clobber forced refresh

A non-forced fetchWorkspacePaths() that started before invalidateWorkspacePaths()
could still resolve afterward, overwrite the cache, and clear forceNextFetch —
making the post-deploy refresh a no-op. Only write back from the promise that
is still the current pending one, and only clear the force flag when the
completing fetch was itself forced.

* refactor(path): drop unreachable 'group' branch in owner-kind setter

The Select only offers user/folder, so the 'group' branch was dead. Leave a
short note pointing at validateName which still accepts 'group' for
forward-compat.

* fix(path): respect disableEditing on owner-kind selector

Other path-editor controls disable on (disabled || disableEditing); the
owner-kind Select only checked `disabled`, so read-only users (trigger
editors with !can_write) could still toggle User/Folder and mutate the
bound path. Reuse the existing nameDisabled flag.

* Revert "fix(autocomplete): don't let stale fetch clobber forced refresh"

This reverts commit 6649975714.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
2026-05-20 13:26:34 +00:00

99 lines
3.1 KiB
Rust

/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2026
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use std::{
sync::{Arc, LazyLock},
time::{Duration, Instant},
};
use axum::{
extract::{Extension, Path, Query},
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
use windmill_common::error::JsonResult;
use crate::db::{ApiAuthed, DB};
// Per-table row cap is inlined into the SQL as `LIMIT 5000`.
// With 6 tables, the absolute ceiling is ~30k paths pre-dedup.
/// Final cap applied after dedup/sort.
const MAX_PATHS: usize = 20_000;
/// TTL for the per-workspace path list cache.
const CACHE_TTL: Duration = Duration::from_secs(60);
/// Workspace-wide path list cache keyed by workspace_id only.
/// One entry per workspace shared across all users — autocomplete is a
/// navigation hint, not an access gate. Saves memory and warms faster.
static PATHS_CACHE: LazyLock<quick_cache::sync::Cache<String, (Arc<Vec<String>>, Instant)>> =
LazyLock::new(|| quick_cache::sync::Cache::new(500));
pub fn workspaced_service() -> Router {
Router::new().route("/list_paths", get(list_paths))
}
#[derive(Serialize)]
struct ListPathsResponse {
paths: Arc<Vec<String>>,
}
#[derive(Deserialize)]
struct ListPathsQuery {
/// When true, bypass the cached entry and re-query the DB, refreshing the
/// cache. Used by clients that just mutated the workspace (e.g. a deploy)
/// and need the new path reflected immediately.
#[serde(default)]
force: bool,
}
async fn list_paths(
_authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Query(ListPathsQuery { force }): Query<ListPathsQuery>,
) -> JsonResult<ListPathsResponse> {
if !force {
if let Some((cached, cached_at)) = PATHS_CACHE.get(&w_id) {
if cached_at.elapsed() < CACHE_TTL {
return Ok(Json(ListPathsResponse { paths: cached }));
}
PATHS_CACHE.remove(&w_id);
}
}
let mut paths: Vec<String> = sqlx::query_scalar!(
r#"
SELECT path AS "path!" FROM (
(SELECT DISTINCT path FROM script WHERE workspace_id = $1 AND archived = false AND deleted = false AND draft_only IS NOT true LIMIT 5000)
UNION
(SELECT path FROM flow WHERE workspace_id = $1 AND archived = false AND draft_only IS NOT true LIMIT 5000)
UNION
(SELECT path FROM app WHERE workspace_id = $1 LIMIT 5000)
UNION
(SELECT path FROM raw_app WHERE workspace_id = $1 LIMIT 5000)
UNION
(SELECT path FROM variable WHERE workspace_id = $1 LIMIT 5000)
UNION
(SELECT path FROM resource WHERE workspace_id = $1 LIMIT 5000)
) t
"#,
&w_id,
)
.fetch_all(&db)
.await?;
paths.sort_unstable();
paths.truncate(MAX_PATHS);
let paths = Arc::new(paths);
PATHS_CACHE.insert(w_id, (paths.clone(), Instant::now()));
Ok(Json(ListPathsResponse { paths }))
}