From a834f210a0d87b66421f8e8d075ee33a43da2167 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Mon, 12 May 2025 18:54:53 +0200 Subject: [PATCH] clan-app: Improved UX of handling toasts --- pkgs/clan-app/clan_app/app.py | 15 ++ pkgs/webview-ui/app/src/api/index.tsx | 45 ++-- .../app/src/components/toast/index.tsx | 236 +++++++++++------- pkgs/webview-ui/app/src/queries/index.ts | 6 +- .../app/src/routes/machines/list.tsx | 2 +- 5 files changed, 192 insertions(+), 112 deletions(-) diff --git a/pkgs/clan-app/clan_app/app.py b/pkgs/clan-app/clan_app/app.py index 3749b9c8e..4446f14d7 100644 --- a/pkgs/clan-app/clan_app/app.py +++ b/pkgs/clan-app/clan_app/app.py @@ -57,6 +57,21 @@ def app_run(app_opts: ClanAppOptions) -> int: status="success", ) + def list_tasks( + *, + op_key: str, + ) -> SuccessDataClass[list[str]] | ErrorDataClass: + """List all tasks.""" + log.info("Listing all tasks.") + with webview.lock: + tasks = list(webview.threads.keys()) + return SuccessDataClass( + op_key=op_key, + data=tasks, + status="success", + ) + + API.overwrite_fn(list_tasks) API.overwrite_fn(open_file) API.overwrite_fn(cancel_task) webview.bind_jsonschema_api(API) diff --git a/pkgs/webview-ui/app/src/api/index.tsx b/pkgs/webview-ui/app/src/api/index.tsx index 22bf87753..204a2f438 100644 --- a/pkgs/webview-ui/app/src/api/index.tsx +++ b/pkgs/webview-ui/app/src/api/index.tsx @@ -5,7 +5,7 @@ import { Schema as Inventory } from "@/api/Inventory"; import { toast, Toast } from "solid-toast"; import { ErrorToastComponent, - InfoToastComponent, + CancelToastComponent, } from "@/src/components/toast"; export type OperationNames = keyof API; export type OperationArgs = API[T]["arguments"]; @@ -62,10 +62,14 @@ const _callApi = ( return { promise, op_key }; }; -const handleCancel = async (ops_key: string) => { +const handleCancel = async ( + ops_key: string, + orig_task: Promise>, +) => { console.log("Canceling operation: ", ops_key); const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key }); const resp = await promise; + if (resp.status === "error") { toast.custom( (t) => ( @@ -79,14 +83,8 @@ const handleCancel = async (ops_key: string) => { }, ); } else { - toast.custom( - (t) => ( - - ), - { - duration: 5000, - }, - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (orig_task as any).cancelled = true; } console.log("Cancel response: ", resp); }; @@ -102,10 +100,10 @@ export const callApi = async ( ( t, // t is the Toast object, t.id is the id of THIS toast instance ) => ( - ), { @@ -114,16 +112,23 @@ export const callApi = async ( ); const response = await promise; - if (response.status === "error") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cancelled = (promise as any).cancelled; + if (cancelled) { + console.log("Not printing toast because operation was cancelled"); + } + + if (response.status === "error" && !cancelled) { toast.remove(toastId); - toast.error( -
- {response.errors.map((err) => ( -

{err.message}

- ))} -
, + toast.custom( + (t) => ( + + ), { - duration: 5000, + duration: Infinity, }, ); } else { diff --git a/pkgs/webview-ui/app/src/components/toast/index.tsx b/pkgs/webview-ui/app/src/components/toast/index.tsx index 745e0618d..4a2b66154 100644 --- a/pkgs/webview-ui/app/src/components/toast/index.tsx +++ b/pkgs/webview-ui/app/src/components/toast/index.tsx @@ -1,5 +1,5 @@ import { toast, Toast } from "solid-toast"; // Make sure to import Toast type -import { Component, JSX } from "solid-js"; +import { Component, JSX, createSignal, onCleanup } from "solid-js"; // --- Icon Components --- @@ -108,43 +108,83 @@ const closeButtonStyle: JSX.CSSProperties = { // Error Toast export const ErrorToastComponent: Component = (props) => { - const handleCancelClick = () => { - if (props.onCancel) { - props.onCancel(); - } - toast.dismiss(props.t.id); + // Local state for click feedback and exit animation + let timeoutId: number | undefined; + const [clicked, setClicked] = createSignal(false); + const [exiting, setExiting] = createSignal(false); + + const handleToastClick = () => { + setClicked(true); + setTimeout(() => { + setExiting(true); + timeoutId = window.setTimeout(() => { + toast.dismiss(props.t.id); + }, 300); // Match exit animation duration + }, 100); // Brief color feedback before animating out }; + // Cleanup timeout if unmounted early + onCleanup(() => { + if (timeoutId) clearTimeout(timeoutId); + }); + return ( -
+
{props.message}
-
); }; - // Info Toast -export const InfoToastComponent: Component = (props) => { - const handleCancelClick = () => { - if (props.onCancel) { - props.onCancel(); - } +export const CancelToastComponent: Component = (props) => { + let timeoutId: number | undefined; + const [clicked, setClicked] = createSignal(false); + const [exiting, setExiting] = createSignal(false); + + // Cleanup timeout if unmounted early + onCleanup(() => { + if (timeoutId) clearTimeout(timeoutId); + }); + + const handleButtonClick = (e: MouseEvent) => { + e.stopPropagation(); + if (props.onCancel) props.onCancel(); toast.dismiss(props.t.id); }; return ( -
+
@@ -152,11 +192,62 @@ export const InfoToastComponent: Component = (props) => { {props.message}
); @@ -164,78 +255,47 @@ export const InfoToastComponent: Component = (props) => { // Warning Toast export const WarningToastComponent: Component = (props) => { - const handleCancelClick = () => { - if (props.onCancel) { - props.onCancel(); - } - toast.dismiss(props.t.id); + let timeoutId: number | undefined; + const [clicked, setClicked] = createSignal(false); + const [exiting, setExiting] = createSignal(false); + + const handleToastClick = () => { + setClicked(true); + setTimeout(() => { + setExiting(true); + timeoutId = window.setTimeout(() => { + toast.dismiss(props.t.id); + }, 300); + }, 100); }; + // Cleanup timeout if unmounted early + onCleanup(() => { + if (timeoutId) clearTimeout(timeoutId); + }); + return ( -
+
{props.message}
-
); }; - -// --- Example Usage --- -/* -import { toast } from 'solid-toast'; -import { - ErrorToastComponent, - InfoToastComponent, - WarningToastComponent -} from './your-toast-components-file'; // Adjust path as necessary - -const logCancel = (type: string) => console.log(`${type} toast cancelled by user.`); - -// Function to show an error toast -export const showErrorToast = (message: string) => { - toast.custom((t) => ( - logCancel('Error')} - /> - ), { duration: Infinity }); // Use Infinity duration if you want it to only close on X click -}; - -// Function to show an info toast -export const showInfoToast = (message: string) => { - toast.custom((t) => ( - - ), { duration: 6000 }); // Or some default duration -}; - -// Function to show a warning toast -export const showWarningToast = (message: string) => { - toast.custom((t) => ( - alert('Warning toast was cancelled!')} - /> - ), { duration: Infinity }); -}; - -// How to use them: -// showErrorToast("Target IP must be provided."); -// showInfoToast("Your profile has been updated successfully."); -// showWarningToast("Your session is about to expire in 5 minutes."); -*/ diff --git a/pkgs/webview-ui/app/src/queries/index.ts b/pkgs/webview-ui/app/src/queries/index.ts index 3b9487a7a..923dabce4 100644 --- a/pkgs/webview-ui/app/src/queries/index.ts +++ b/pkgs/webview-ui/app/src/queries/index.ts @@ -23,7 +23,7 @@ export const createModulesQuery = ( base_path: uri, }); if (response.status === "error") { - toast.error("Failed to fetch data"); + console.error("Failed to fetch data"); } else { console.log(response.data.localModules["hello-world"]["manifest"]); return response.data; @@ -47,7 +47,7 @@ export const tagsQuery = (uri: string | null) => flake: { identifier: uri }, }); if (response.status === "error") { - toast.error("Failed to fetch data"); + console.error("Failed to fetch data"); } else { const machines = response.data.machines || {}; const tags = Object.values(machines).flatMap((m) => m.tags || []); @@ -68,7 +68,7 @@ export const machinesQuery = (uri: string | null) => flake: { identifier: uri }, }); if (response.status === "error") { - toast.error("Failed to fetch data"); + console.error("Failed to fetch data"); } else { const machines = response.data.machines || {}; return Object.keys(machines); diff --git a/pkgs/webview-ui/app/src/routes/machines/list.tsx b/pkgs/webview-ui/app/src/routes/machines/list.tsx index 3de06a04e..fc751cc27 100644 --- a/pkgs/webview-ui/app/src/routes/machines/list.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/list.tsx @@ -44,7 +44,7 @@ export const MachineListView: Component = () => { }, }); if (response.status === "error") { - toast.error("Failed to fetch data"); + console.error("Failed to fetch data"); } else { return response.data; }