Skip to content

feat(plugins): Add UI feedback for plugin operations and refactor han…#51

Merged
Its4Nik merged 2 commits intodevfrom
fix-double-installation
Jan 7, 2026
Merged

feat(plugins): Add UI feedback for plugin operations and refactor han…#51
Its4Nik merged 2 commits intodevfrom
fix-double-installation

Conversation

@Its4Nik
Copy link
Copy Markdown
Owner

@Its4Nik Its4Nik commented Jan 7, 2026

…dler

Implemented toast notifications in the UI for plugin installation and uninstallation, providing immediate user feedback with success or error messages. This improves the user experience by making plugin operations more transparent.

Refactored the plugin handler's savePlugin method to enhance readability, maintainability, and error handling. Key logic has been extracted into dedicated private helper functions: isDuplicatePlugin, ensurePluginBundle, checkPluginSafety, updateExistingPlugin, and insertNewPlugin. All plugin operation results now return a consistent { success: boolean, message: string } object.

Additionally, the verificationApi column was removed from the plugin database schema, and the Toast component's default button variant was updated from "outline" to "secondary" for non-error messages.

Summary by Sourcery

Add user-facing feedback for plugin install/uninstall operations and refactor plugin persistence and safety checks for clearer flow and consistent result reporting.

New Features:

  • Show toast notifications in the UI for plugin installation and uninstallation outcomes, including success and error states.

Enhancements:

  • Refactor plugin handler plugin-saving logic into smaller helper methods for duplicate detection, bundle retrieval, safety verification, updates, and inserts, returning a standardized success/message result.
  • Simplify plugin verification result handling with unified error messaging and logging.
  • Remove the unused verificationApi column from the plugin database schema.
  • Adjust the Toast component’s default action button style to use the secondary variant for non-error messages.

…dler

Implemented toast notifications in the UI for plugin installation and uninstallation, providing immediate user feedback with success or error messages. This improves the user experience by making plugin operations more transparent.

Refactored the plugin handler's `savePlugin` method to enhance readability, maintainability, and error handling. Key logic has been extracted into dedicated private helper functions: `isDuplicatePlugin`, `ensurePluginBundle`, `checkPluginSafety`, `updateExistingPlugin`, and `insertNewPlugin`. All plugin operation results now return a consistent `{ success: boolean, message: string }` object.

Additionally, the `verificationApi` column was removed from the plugin database schema, and the `Toast` component's default button variant was updated from "outline" to "secondary" for non-error messages.
@Its4Nik Its4Nik self-assigned this Jan 7, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jan 7, 2026

Reviewer's Guide

Adds user-facing toast notifications for plugin install/uninstall operations, refactors plugin saving logic into smaller helpers with consistent result objects, removes an unused DB column, and adjusts the default toast button style for non-error messages.

Sequence diagram for plugin install with UI toast feedback

sequenceDiagram
  actor User
  participant PluginBrowser
  participant ReactQuery
  participant Backend as PluginHandler
  participant Toast

  User->>PluginBrowser: Click install on plugin
  PluginBrowser->>ReactQuery: mutateAsync(pluginPayload)
  ReactQuery->>Backend: savePlugin(plugin, update=false)
  Backend->>Backend: isDuplicatePlugin(name)
  alt duplicate plugin
    Backend-->>ReactQuery: { success: false, message }
  else not duplicate
    Backend->>Backend: ensurePluginBundle(plugin)
    alt bundle fetch fails
      Backend-->>ReactQuery: { success: false, message }
    else bundle ok
      Backend->>Backend: checkPluginSafety(plugin)
      alt plugin unsafe or unverified
        Backend-->>ReactQuery: { success: false, message }
      else plugin safe
        Backend->>Backend: insertNewPlugin(plugin)
        Backend-->>ReactQuery: { success: true, message, id }
      end
    end
  end

  ReactQuery-->>PluginBrowser: mutation result
  PluginBrowser->>Toast: toast({ title, description: result.message, variant })
  Toast-->>User: Visual notification of install success or error
Loading

Sequence diagram for plugin uninstall with UI toast feedback

sequenceDiagram
  actor User
  participant PluginBrowser
  participant deletePluginMutation as ReactQuery
  participant PluginHandler as Backend
  participant toast as Toast

  User->>PluginBrowser: Click uninstall on plugin
  PluginBrowser->>deletePluginMutation: mutateAsync(id)
  deletePluginMutation->>PluginHandler: deletePlugin(id)
  PluginHandler-->>deletePluginMutation: { success: boolean, message: string }
  deletePluginMutation-->>PluginBrowser: mutation result
  PluginBrowser->>toast: toast({ title, description: result.message, variant })
  toast-->>User: Visual notification of uninstall success or error
Loading

ER diagram for updated plugin table without verificationApi

erDiagram
  PLUGIN {
    int id PK
    text name
    text repoType
    text repository
    text manifest
    json author
    text plugin
  }
Loading

Class diagram for refactored PluginHandler savePlugin workflow

classDiagram
  class PluginHandler {
    - logger
    - table
    + savePlugin(plugin: DBPluginShemaT, update: boolean) Result
    - isDuplicatePlugin(name: string) boolean
    - ensurePluginBundle(plugin: DBPluginShemaT) Result
    - checkPluginSafety(plugin: DBPluginShemaT) Result
    - updateExistingPlugin(plugin: DBPluginShemaT) Result
    - insertNewPlugin(plugin: DBPluginShemaT) Result
    + getAll() DBPluginShemaT[]
    + verifyPlugin(plugin: DBPluginShemaT) VerificationResult
    + unloadPlugin(id: number) void
    + deletePlugin(id: number) void
    + loadPlugin(id: number) void
  }

  class DBPluginShemaT {
    + id: number
    + name: string
    + repoType: string
    + repository: string
    + manifest: string
    + author: json
    + plugin: string
  }

  class Result {
    + success: boolean
    + message: string
    + id: number
  }

  class VerificationResult {
    <<union>>
    + value: number
    + value: boolean
    + value: string
  }

  PluginHandler --> DBPluginShemaT : manages
  PluginHandler --> Result : returns
  PluginHandler --> VerificationResult : uses
Loading

File-Level Changes

Change Details Files
Refactor plugin saving logic into smaller, reusable helpers with consistent success/error result objects.
  • Reworked savePlugin to orchestrate duplicate checking, bundle fetching, safety verification, and branching between update and insert flows.
  • Added isDuplicatePlugin to prevent installing plugins with an already-used name when not in update mode.
  • Added ensurePluginBundle to lazily fetch the plugin bundle with retry, structured logging, and explicit error messaging when fetching fails.
  • Added checkPluginSafety to encapsulate verifyPlugin outcomes and normalize them into success/failure with user-facing messages.
  • Added updateExistingPlugin and insertNewPlugin helpers so updates unload/delete then reinsert and reload plugins while inserts log and persist the plugin to the DB.
packages/plugin-handler/src/index.ts
Remove the verificationApi column from the plugin database schema.
  • Deleted the verificationApi column definition from the plugin table schema while preserving the rest of the table configuration.
packages/plugin-handler/src/index.ts
Surface plugin install/uninstall results to the user via toast notifications in the UI.
  • Wrapped deletePluginMutation.mutateAsync in handleDelete to capture the result and show a toast with success or error variant based on the response.
  • Wrapped installPluginMutation.mutateAsync in handleInstall to capture the result and show a toast summarizing plugin installation outcome.
apps/dockstat/src/pages/extensions/plugins.tsx
Adjust default toast button styling for non-error toasts to be more prominent.
  • Changed the Toast component action button variant from outline to secondary when the toast is not an error, keeping danger for error toasts.
apps/dockstat/src/components/toast.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The helper methods used by savePlugin (ensurePluginBundle, checkPluginSafety, insertNewPlugin, updateExistingPlugin) don’t consistently return the { success: boolean, message: string } shape described in the PR (e.g., some success paths omit message), which makes the API harder to reason about and could break callers expecting a uniform result object.
  • updateExistingPlugin assumes insertNewPlugin returns an object with an id field (res.id && this.loadPlugin(res.id)), but insertNewPlugin appears to still return only { success, message } and already performs loadPlugin, so this code path is either redundant or incorrect and should be aligned with the actual return value.
  • The install/uninstall toasts always use a success-oriented title (Installed X, Uninstalled PluginID: Y) even on failure; consider adjusting the title based on res.success so users get clearer feedback when an operation fails.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The helper methods used by `savePlugin` (`ensurePluginBundle`, `checkPluginSafety`, `insertNewPlugin`, `updateExistingPlugin`) don’t consistently return the `{ success: boolean, message: string }` shape described in the PR (e.g., some success paths omit `message`), which makes the API harder to reason about and could break callers expecting a uniform result object.
- `updateExistingPlugin` assumes `insertNewPlugin` returns an object with an `id` field (`res.id && this.loadPlugin(res.id)`), but `insertNewPlugin` appears to still return only `{ success, message }` and already performs `loadPlugin`, so this code path is either redundant or incorrect and should be aligned with the actual return value.
- The install/uninstall toasts always use a success-oriented title (`Installed X`, `Uninstalled PluginID: Y`) even on failure; consider adjusting the title based on `res.success` so users get clearer feedback when an operation fails.

## Individual Comments

### Comment 1
<location> `packages/plugin-handler/src/index.ts:194-203` </location>
<code_context>
+      return { success: true }
+    }
+
+    try {
+      this.logger.info(`Plugin ${plugin.name} has no bundle, fetching...`)
+      plugin.plugin = await retry(
+        () => repo.getPluginBundle(plugin.repoType, `${plugin.repository}/${plugin.manifest}`),
         { attempts: 3, delay: 2000 }
       )
+      return { success: true }
+    } catch (error) {
+      return {
+        success: false,
+        message: `Failed to fetch plugin bundle: ${JSON.stringify(error)}`,
+      }
     }
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Serializing the error with `JSON.stringify` can be brittle and may expose internal details.

`JSON.stringify(error)` can throw on circular references and risks exposing sensitive internals (stack traces, config) in user-facing responses. Instead, log the full error and return a generic message:

```ts
} catch (error) {
  this.logger.error({ error }, `Failed to fetch plugin bundle for ${plugin.name}`)
  return {
    success: false,
    message: "Failed to fetch plugin bundle. Please try again later.",
  }
}
```
If you need limited details in the response, use `String(error)` or `error instanceof Error ? error.message : String(error)` for the message field.
</issue_to_address>

### Comment 2
<location> `packages/plugin-handler/src/index.ts:235-241` </location>
<code_context>
+    return { success: false, message }
+  }
+
+  private updateExistingPlugin(plugin: DBPluginShemaT) {
+    this.logger.info(`Updating Plugin ${plugin.name}`)
+    this.unloadPlugin(Number(plugin.id))
+    this.deletePlugin(Number(plugin.id))
+    const res = this.insertNewPlugin(plugin)
+    res.id && this.loadPlugin(res.id)
+    return res
+  }
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Updating by delete-then-insert can leave the system without the plugin if insertion fails.

The current flow:

```ts
this.unloadPlugin(Number(plugin.id))
this.deletePlugin(Number(plugin.id))
const res = this.insertNewPlugin(plugin)
res.id && this.loadPlugin(res.id)
```

means that if `insertNewPlugin` fails, the plugin is permanently removed during an "update". To avoid this, consider:

1. Updating the existing row in place where possible, or
2. Inserting the new plugin first, then deleting the old one (with rollback/error handling), or
3. At least making the error clearly state that the previous version was removed and the new one failed.

If this behavior is intentional, also gate `loadPlugin` on an explicit success flag rather than just `res.id` to avoid loading an invalid/partial record.
</issue_to_address>

### Comment 3
<location> `apps/dockstat/src/pages/extensions/plugins.tsx:79-74` </location>
<code_context>

   const handleInstall = async (plugin: AvailablePlugin) => {
-    await installPluginMutation.mutateAsync({
+    const res = await installPluginMutation.mutateAsync({
       id: plugin.isInstalled ? null : null,
       plugin: "", // handled by backend
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The toast title always says "Installed" even when the installation fails.

Since `variant` can be `"error"`, consider making the title depend on `res.success`, for example:

```ts
toast({
  title: res.success ? `Installed ${plugin.name}` : `Failed to install ${plugin.name}`,
  description: res.message,
  variant: res.success ? "success" : "error",
})
```
</issue_to_address>

### Comment 4
<location> `apps/dockstat/src/pages/extensions/plugins.tsx:80` </location>
<code_context>
   const handleInstall = async (plugin: AvailablePlugin) => {
-    await installPluginMutation.mutateAsync({
+    const res = await installPluginMutation.mutateAsync({
       id: plugin.isInstalled ? null : null,
       plugin: "", // handled by backend
       name: plugin.name,
</code_context>

<issue_to_address>
**issue (bug_risk):** The ternary `plugin.isInstalled ? null : null` is redundant and can be simplified.

This always evaluates to `null`, so you can replace it with `id: null`. If different values are expected based on `plugin.isInstalled`, this is likely a typo and the branches should be updated accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/plugin-handler/src/index.ts
Comment thread packages/plugin-handler/src/index.ts
Comment thread apps/dockstat/src/pages/extensions/plugins.tsx
Comment thread apps/dockstat/src/pages/extensions/plugins.tsx Outdated
- Ensure correct `installedId` is passed when attempting to update/reinstall an existing plugin.
- Add robust error handling during plugin updates to prevent inconsistent states if the previous version fails to uninstall.
- Improve toast messages for plugin install/uninstall success and failure scenarios, providing clearer feedback to the user.
- Format error messages more gracefully when fetching plugin bundles, displaying the error message directly instead of stringifying the entire error object.
@Its4Nik Its4Nik merged commit 0028858 into dev Jan 7, 2026
3 checks passed
@Its4Nik Its4Nik deleted the fix-double-installation branch January 7, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant