mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
dedup folder labels to prevent each_key_duplicate crash (#9565)
Folder labels are exposed verbatim as `inherited_labels` (via the
`folder_labels` SQL function) and rendered in keyed `{#each}` blocks that
throw Svelte's `each_key_duplicate` on a repeated key, crashing the list
views. The UI dedups labels on entry, but API / CLI / git-sync writes do
not, so a folder.yaml with `labels: [foo, foo]` persists duplicates.
- Dedup on write in create_folder and update_folder (order-preserving).
- Make folder_labels() dedup on read so it is resilient regardless of how a
row was populated, plus a one-time cleanup of already-persisted duplicates
so direct folder.labels reads (folder list, editor) are safe too.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
-- Restore the original passthrough definition (the one-time data cleanup is not reverted).
|
||||
CREATE OR REPLACE FUNCTION folder_labels(w_id text, item_path text) RETURNS text[]
|
||||
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT labels FROM folder
|
||||
WHERE workspace_id = w_id AND item_path LIKE 'f/%' AND name = split_part(item_path, '/', 2)
|
||||
$$;
|
||||
@@ -0,0 +1,33 @@
|
||||
-- Folder labels are exposed verbatim as `inherited_labels` (via folder_labels) and
|
||||
-- rendered in keyed `{#each}` blocks in the UI, which throw `each_key_duplicate` on a
|
||||
-- repeated key. The write paths now dedup, but make the read resilient regardless of
|
||||
-- how a row was populated, and clean up any duplicates already persisted.
|
||||
|
||||
-- Dedup while preserving first-seen order.
|
||||
CREATE OR REPLACE FUNCTION folder_labels(w_id text, item_path text) RETURNS text[]
|
||||
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT (
|
||||
SELECT array_agg(l ORDER BY first_ord)
|
||||
FROM (
|
||||
SELECT u.l, min(u.ord) AS first_ord
|
||||
FROM unnest(f.labels) WITH ORDINALITY AS u(l, ord)
|
||||
GROUP BY u.l
|
||||
) deduped
|
||||
)
|
||||
FROM folder f
|
||||
WHERE f.workspace_id = w_id AND item_path LIKE 'f/%' AND f.name = split_part(item_path, '/', 2)
|
||||
$$;
|
||||
|
||||
-- One-time cleanup of rows that already contain duplicate labels, so direct reads of
|
||||
-- folder.labels (folder list, editor) are also safe.
|
||||
UPDATE folder
|
||||
SET labels = (
|
||||
SELECT array_agg(l ORDER BY first_ord)
|
||||
FROM (
|
||||
SELECT u.l, min(u.ord) AS first_ord
|
||||
FROM unnest(labels) WITH ORDINALITY AS u(l, ord)
|
||||
GROUP BY u.l
|
||||
) deduped
|
||||
)
|
||||
WHERE labels IS NOT NULL
|
||||
AND cardinality(labels) <> (SELECT count(DISTINCT x) FROM unnest(labels) AS x);
|
||||
@@ -86,6 +86,14 @@ pub struct UpdateFolder {
|
||||
pub labels: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// Folder labels are surfaced verbatim as `inherited_labels` and rendered in keyed
|
||||
// `{#each}` blocks; a repeated label is a duplicate key that crashes the list views.
|
||||
// The UI dedups on entry but API/CLI/git-sync writes do not, so normalize on write.
|
||||
fn dedup_labels(labels: &mut Vec<String>) {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
labels.retain(|l| seen.insert(l.clone()));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Owner {
|
||||
pub owner: String,
|
||||
@@ -226,8 +234,11 @@ async fn create_folder(
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(cache): Extension<Arc<AuthCache>>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(ng): Json<NewFolder>,
|
||||
Json(mut ng): Json<NewFolder>,
|
||||
) -> Result<String> {
|
||||
if let Some(labels) = ng.labels.as_mut() {
|
||||
dedup_labels(labels);
|
||||
}
|
||||
if let RuleCheckResult::Blocked(msg) = check_deploy_rules(
|
||||
&w_id,
|
||||
AuditAuthorable::username(&authed),
|
||||
@@ -471,7 +482,8 @@ async fn update_folder(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(labels) = ng.labels.as_ref() {
|
||||
if let Some(labels) = ng.labels.as_mut() {
|
||||
dedup_labels(labels);
|
||||
if labels.is_empty() {
|
||||
// normalize cleared labels to NULL so the field stays out of API/tarball output
|
||||
sqlb.set("labels", "NULL");
|
||||
|
||||
Reference in New Issue
Block a user