diff --git a/backend/fluksio/flow/dashboards.py b/backend/fluksio/flow/dashboards.py index 7286940..73dcf76 100644 --- a/backend/fluksio/flow/dashboards.py +++ b/backend/fluksio/flow/dashboards.py @@ -26,7 +26,13 @@ from pydantic import BaseModel, Field, field_validator, model_validator from fluksio.flow.messages import DType, qualify from fluksio.flow.schemas import FlowDef, _validate_name -from fluksio.flow.store import FlowStore, StaleVersion, _updated_at, checked +from fluksio.flow.store import ( + FlowStore, + StaleVersion, + _updated_at, + checked, + copy_name, +) #: Sibling of the shared-node library, and likewise not a flow. DASHBOARD_DIR = "_dashboards" @@ -854,10 +860,7 @@ class DashboardStore: """ with self._lock, self.flows._write_lock: source = self.read(name, draft=True) - new_name, count = f"{name}_copy", 1 - while self.exists(new_name): - count += 1 - new_name = f"{name}_copy_{count}" + new_name = copy_name(name, self.exists) copy = source.model_copy( update={ "name": new_name, diff --git a/backend/fluksio/flow/store.py b/backend/fluksio/flow/store.py index f2ea6b2..e37de9e 100644 --- a/backend/fluksio/flow/store.py +++ b/backend/fluksio/flow/store.py @@ -16,10 +16,11 @@ from __future__ import annotations import json import logging +import re import shutil import subprocess import threading -from collections.abc import Iterable +from collections.abc import Callable, Iterable from datetime import UTC, datetime from pathlib import Path @@ -134,6 +135,16 @@ def _same_content(left: FlowDef, right: FlowDef) -> bool: ) +def copy_name(name: str, taken: Callable[[str], bool]) -> str: + """``{name}_copy``, then ``_copy_2`` and on, from a copy's own original.""" + base = re.sub(r"_copy(_\d+)?$", "", name) + new_name, count = f"{base}_copy", 1 + while taken(new_name): + count += 1 + new_name = f"{base}_copy_{count}" + return new_name + + class FlowStore: """Reads and writes flows, committing every change.""" @@ -698,12 +709,9 @@ class FlowStore: """ with self._write_lock: source = self.read_flow(name, draft=True) - new_name, count = f"{name}_copy", 1 # The directory rather than `exists`: anything left in it would # otherwise end up in the copy. - while self._flow_dir(new_name).exists(): - count += 1 - new_name = f"{name}_copy_{count}" + new_name = copy_name(name, lambda n: self._flow_dir(n).exists()) copy = source.model_copy( update={ diff --git a/frontend/src/components/Common/OverviewToolbar.tsx b/frontend/src/components/Common/OverviewToolbar.tsx index 9bfff7f..11be985 100644 --- a/frontend/src/components/Common/OverviewToolbar.tsx +++ b/frontend/src/components/Common/OverviewToolbar.tsx @@ -3,6 +3,7 @@ import { Check, Copy, Plus, Search, Trash2 } from "lucide-react" import { motion } from "motion/react" import { type ReactNode, useRef, useState } from "react" +import type { ApiError } from "@/client" import { Button } from "@/components/ui/button" import { Dialog, @@ -22,6 +23,7 @@ import { } from "@/components/ui/tooltip" import useCustomToast from "@/hooks/useCustomToast" import { transitions } from "@/lib/motion" +import { extractErrorMessage } from "@/utils" /** * The bar over the flows and dashboards lists: find one, publish what is @@ -233,8 +235,8 @@ function useBatch( for (const name of names) { try { await act(name) - } catch { - failed.push(name) + } catch (err) { + failed.push(`${name} (${extractErrorMessage(err as ApiError)})`) } } return failed