mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-25 10:08:29 +02:00
Fixed bugs in Plugin settings pane
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
use crate::error::Error::MigrationError;
|
||||
use crate::error::Result;
|
||||
use include_dir::{include_dir, Dir, DirEntry};
|
||||
use log::{debug, info};
|
||||
use include_dir::{Dir, DirEntry, include_dir};
|
||||
use log::info;
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::{params, OptionalExtension, TransactionBehavior};
|
||||
use rusqlite::{OptionalExtension, TransactionBehavior, params};
|
||||
use sha2::{Digest, Sha384};
|
||||
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
@@ -40,12 +40,18 @@ pub(crate) fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
|
||||
|
||||
// Run each migration in a transaction
|
||||
let mut num_migrations = 0;
|
||||
let mut ran_migrations = 0;
|
||||
for entry in entries {
|
||||
num_migrations += 1;
|
||||
let mut conn = pool.get()?;
|
||||
let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||
match run_migration(entry, &mut tx) {
|
||||
Ok(_) => tx.commit()?,
|
||||
Ok(ran) => {
|
||||
if ran {
|
||||
ran_migrations += 1;
|
||||
}
|
||||
tx.commit()?
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!(
|
||||
"{} failed with {}",
|
||||
@@ -58,7 +64,11 @@ pub(crate) fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
|
||||
};
|
||||
}
|
||||
|
||||
info!("Finished running {} migrations", num_migrations);
|
||||
if ran_migrations == 0 {
|
||||
info!("No migrations to run out of {}", num_migrations);
|
||||
} else {
|
||||
info!("Ran {}/{} migrations", ran_migrations, num_migrations);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -76,9 +86,7 @@ fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> R
|
||||
.optional()?;
|
||||
|
||||
if row.is_some() {
|
||||
debug!("Skipping migration {description}");
|
||||
// Migration was already run
|
||||
return Ok(false);
|
||||
return Ok(false); // Migration was already run
|
||||
}
|
||||
|
||||
let sql =
|
||||
|
||||
@@ -43,8 +43,10 @@ pub async fn download_plugin_archive<R: Runtime>(
|
||||
};
|
||||
let resp = yaak_api_client(app_handle)?.get(url.clone()).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
warn!("Failed to download plugin: {name} {version}");
|
||||
return Err(ApiErr(format!("{} response to {}", resp.status(), url.to_string())));
|
||||
}
|
||||
info!("Downloaded plugin: {url}");
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ use yaak_models::models::Plugin;
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
|
||||
pub async fn delete_and_uninstall<R: Runtime>(window: &WebviewWindow<R>, plugin_id: &str) -> Result<Plugin> {
|
||||
pub async fn delete_and_uninstall<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
plugin_id: &str,
|
||||
) -> Result<Plugin> {
|
||||
let plugin_manager = window.state::<PluginManager>();
|
||||
let plugin = window.db().delete_plugin_by_id(plugin_id, &UpdateSource::from_window(&window))?;
|
||||
plugin_manager.uninstall(&PluginWindowContext::new(&window), plugin.directory.as_str()).await?;
|
||||
@@ -25,6 +28,7 @@ pub async fn download_and_install<R: Runtime>(
|
||||
name: &str,
|
||||
version: Option<String>,
|
||||
) -> Result<PluginVersion> {
|
||||
info!("Installing plugin {} {}", name, version.clone().unwrap_or_default());
|
||||
let plugin_manager = window.state::<PluginManager>();
|
||||
let plugin_version = get_plugin(window.app_handle(), name, version).await?;
|
||||
let resp = download_plugin_archive(window.app_handle(), &plugin_version).await?;
|
||||
|
||||
@@ -209,6 +209,7 @@ impl PluginManager {
|
||||
dir: &str,
|
||||
) -> Result<()> {
|
||||
info!("Adding plugin by dir {dir}");
|
||||
|
||||
let maybe_tx = self.ws_service.app_to_plugin_events_tx.lock().await;
|
||||
let tx = match &*maybe_tx {
|
||||
None => return Err(ClientNotInitializedErr),
|
||||
@@ -233,6 +234,12 @@ impl PluginManager {
|
||||
)
|
||||
.await??;
|
||||
|
||||
let mut plugins = self.plugins.lock().await;
|
||||
|
||||
// Remove the existing plugin (if exists) before adding this one
|
||||
plugins.retain(|p| p.dir != dir);
|
||||
plugins.push(plugin_handle.clone());
|
||||
|
||||
// Add the new plugin
|
||||
self.plugins.lock().await.push(plugin_handle.clone());
|
||||
|
||||
|
||||
@@ -9,17 +9,17 @@ import {
|
||||
searchPlugins,
|
||||
uninstallPlugin,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import type { PluginUpdatesResponse } from '@yaakapp-internal/plugins/bindings/gen_api';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import React, { useState } from 'react';
|
||||
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
|
||||
import { useInstallPlugin } from '../../hooks/useInstallPlugin';
|
||||
import { usePluginInfo } from '../../hooks/usePluginInfo';
|
||||
import { useRefreshPlugins } from '../../hooks/usePlugins';
|
||||
import { usePluginsKey, useRefreshPlugins } from '../../hooks/usePlugins';
|
||||
import { showConfirmDelete } from '../../lib/confirm';
|
||||
import { minPromiseMillis } from '../../lib/minPromiseMillis';
|
||||
import { Button } from '../core/Button';
|
||||
import { CountBadge } from '../core/CountBadge';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { InlineCode } from '../core/InlineCode';
|
||||
import { Link } from '../core/Link';
|
||||
@@ -57,7 +57,7 @@ export function SettingsPlugins() {
|
||||
<TabContent value="search">
|
||||
<PluginSearch />
|
||||
</TabContent>
|
||||
<TabContent value="installed">
|
||||
<TabContent value="installed" className="pb-0">
|
||||
<div className="h-full grid grid-rows-[minmax(0,1fr)_auto]">
|
||||
<InstalledPlugins />
|
||||
<footer className="grid grid-cols-[minmax(0,1fr)_auto] -mx-4 py-2 px-4 border-t bg-surface-highlight border-border-subtle min-w-0">
|
||||
@@ -107,32 +107,64 @@ export function SettingsPlugins() {
|
||||
);
|
||||
}
|
||||
|
||||
function PluginTableRow({
|
||||
plugin,
|
||||
updates,
|
||||
}: {
|
||||
plugin: Plugin;
|
||||
updates: PluginUpdatesResponse | null;
|
||||
}) {
|
||||
const pluginInfo = usePluginInfo(plugin.id);
|
||||
const latestVersion = updates?.plugins.find((u) => u.name === pluginInfo.data?.name)?.version;
|
||||
const installPluginMutation = useMutation({
|
||||
mutationKey: ['install_plugin', plugin.id],
|
||||
mutationFn: (name: string) => installPlugin(name, null),
|
||||
});
|
||||
|
||||
const displayName = pluginInfo.data?.displayName ?? 'Unknown';
|
||||
const uninstallPluginMutation = usePromptUninstall(plugin.id, displayName);
|
||||
|
||||
if (pluginInfo.isPending) {
|
||||
function PluginTableRowForInstalledPlugin({ plugin }: { plugin: Plugin }) {
|
||||
const info = usePluginInfo(plugin.id).data;
|
||||
if (info == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PluginTableRow
|
||||
plugin={plugin}
|
||||
version={info.version}
|
||||
name={info.name}
|
||||
displayName={info.displayName}
|
||||
url={plugin.url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PluginTableRowForRemotePluginVersion({ pluginVersion }: { pluginVersion: PluginVersion }) {
|
||||
const plugin = useAtomValue(pluginsAtom).find((p) => p.id === pluginVersion.id);
|
||||
const pluginInfo = usePluginInfo(plugin?.id ?? null).data;
|
||||
|
||||
return (
|
||||
<PluginTableRow
|
||||
plugin={plugin ?? null}
|
||||
version={pluginInfo?.version ?? pluginVersion.version}
|
||||
name={pluginVersion.name}
|
||||
displayName={pluginVersion.displayName}
|
||||
url={pluginVersion.url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PluginTableRow({
|
||||
plugin,
|
||||
name,
|
||||
version,
|
||||
displayName,
|
||||
url,
|
||||
}: {
|
||||
plugin: Plugin | null;
|
||||
name: string;
|
||||
version: string;
|
||||
displayName: string;
|
||||
url: string | null;
|
||||
}) {
|
||||
const updates = usePluginUpdates();
|
||||
const latestVersion = updates.data?.plugins.find((u) => u.name === name)?.version;
|
||||
const installPluginMutation = useMutation({
|
||||
mutationKey: ['install_plugin', name],
|
||||
mutationFn: (name: string) => installPlugin(name, null),
|
||||
});
|
||||
const uninstall = usePromptUninstall(plugin?.id ?? null, displayName);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className="font-semibold">
|
||||
{plugin.url ? (
|
||||
<Link noUnderline href={plugin.url}>
|
||||
{url ? (
|
||||
<Link noUnderline href={url}>
|
||||
{displayName}
|
||||
</Link>
|
||||
) : (
|
||||
@@ -140,33 +172,52 @@ function PluginTableRow({
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<InlineCode>{pluginInfo.data?.version ?? 'n/a'}</InlineCode>
|
||||
<HStack space={1.5}>
|
||||
<InlineCode>{version}</InlineCode>
|
||||
{latestVersion != null && (
|
||||
<InlineCode className="text-success flex items-center gap-1">
|
||||
<Icon icon="arrow_up" size="sm" />
|
||||
{latestVersion}
|
||||
</InlineCode>
|
||||
)}
|
||||
</HStack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<HStack justifyContent="end">
|
||||
{pluginInfo.data && latestVersion != null && (
|
||||
<TableCell className="!py-0">
|
||||
<HStack justifyContent="end" space={1.5}>
|
||||
{plugin != null && latestVersion != null ? (
|
||||
<Button
|
||||
variant="border"
|
||||
color="success"
|
||||
title={`Update to ${latestVersion}`}
|
||||
size="xs"
|
||||
isLoading={installPluginMutation.isPending}
|
||||
onClick={() => installPluginMutation.mutate(pluginInfo.data.name)}
|
||||
onClick={() => installPluginMutation.mutate(name)}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
) : plugin == null ? (
|
||||
<Button
|
||||
variant="border"
|
||||
color="primary"
|
||||
title={`Install ${latestVersion}`}
|
||||
size="xs"
|
||||
isLoading={installPluginMutation.isPending}
|
||||
onClick={() => installPluginMutation.mutate(name)}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
) : null}
|
||||
{uninstall != null && (
|
||||
<Button
|
||||
size="xs"
|
||||
title="Uninstall plugin"
|
||||
variant="border"
|
||||
isLoading={uninstall.isPending}
|
||||
onClick={() => uninstall.mutate()}
|
||||
>
|
||||
Uninstall
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="xs"
|
||||
title="Uninstall plugin"
|
||||
variant="border"
|
||||
isLoading={uninstallPluginMutation.isPending}
|
||||
onClick={async () => {
|
||||
uninstallPluginMutation.mutate();
|
||||
}}
|
||||
>
|
||||
Uninstall
|
||||
</Button>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -209,20 +260,8 @@ function PluginSearch() {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{results.data.plugins.map((plugin) => (
|
||||
<TableRow key={plugin.id}>
|
||||
<TableCell className="font-semibold">
|
||||
<Link noUnderline href={plugin.url}>
|
||||
{plugin.displayName}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<InlineCode>{plugin.version}</InlineCode>
|
||||
</TableCell>
|
||||
<TableCell className="w-[6rem]">
|
||||
<InstallPluginButton pluginVersion={plugin} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{results.data.plugins.map((p) => (
|
||||
<PluginTableRowForRemotePluginVersion key={p.id} pluginVersion={p} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -232,41 +271,8 @@ function PluginSearch() {
|
||||
);
|
||||
}
|
||||
|
||||
function InstallPluginButton({ pluginVersion }: { pluginVersion: PluginVersion }) {
|
||||
const plugins = useAtomValue(pluginsAtom);
|
||||
const installed = plugins?.some((p) => p.id === pluginVersion.id);
|
||||
const uninstallPluginMutation = usePromptUninstall(pluginVersion.id, pluginVersion.displayName);
|
||||
const installPluginMutation = useMutation({
|
||||
mutationKey: ['install_plugin', pluginVersion.id],
|
||||
mutationFn: (pv: PluginVersion) => installPlugin(pv.name, null),
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="border"
|
||||
color={installed ? 'default' : 'primary'}
|
||||
className="ml-auto"
|
||||
isLoading={installPluginMutation.isPending || uninstallPluginMutation.isPending}
|
||||
onClick={async () => {
|
||||
if (installed) {
|
||||
uninstallPluginMutation.mutate();
|
||||
} else {
|
||||
installPluginMutation.mutate(pluginVersion);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{installed ? 'Uninstall' : 'Install'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function InstalledPlugins() {
|
||||
const plugins = useAtomValue(pluginsAtom);
|
||||
const updates = useQuery({
|
||||
queryKey: ['plugin_updates'],
|
||||
queryFn: () => checkPluginUpdates(),
|
||||
});
|
||||
|
||||
return plugins.length === 0 ? (
|
||||
<div className="pb-4">
|
||||
@@ -286,18 +292,20 @@ function InstalledPlugins() {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<tbody className="divide-y divide-surface-highlight">
|
||||
{plugins.map((p) => {
|
||||
return <PluginTableRow key={p.id} plugin={p} updates={updates.data ?? null} />;
|
||||
})}
|
||||
{plugins.map((p) => (
|
||||
<PluginTableRowForInstalledPlugin key={p.id} plugin={p} />
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function usePromptUninstall(pluginId: string, name: string) {
|
||||
return useMutation({
|
||||
function usePromptUninstall(pluginId: string | null, name: string) {
|
||||
const mut = useMutation({
|
||||
mutationKey: ['uninstall_plugin', pluginId],
|
||||
mutationFn: async () => {
|
||||
if (pluginId == null) return;
|
||||
|
||||
const confirmed = await showConfirmDelete({
|
||||
id: 'uninstall-plugin-' + pluginId,
|
||||
title: 'Uninstall Plugin',
|
||||
@@ -313,4 +321,13 @@ function usePromptUninstall(pluginId: string, name: string) {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return pluginId == null ? null : mut;
|
||||
}
|
||||
|
||||
function usePluginUpdates() {
|
||||
return useQuery({
|
||||
queryKey: ['plugin_updates', usePluginsKey()],
|
||||
queryFn: () => checkPluginUpdates(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ export const TabContent = memo(function TabContent({
|
||||
<div
|
||||
tabIndex={-1}
|
||||
data-tab={value}
|
||||
className={classNames(className, 'tab-content', 'hidden w-full h-full py-2')}
|
||||
className={classNames(className, 'tab-content', 'hidden w-full h-full pt-2')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -6,18 +6,21 @@ import { useAtomValue } from 'jotai';
|
||||
import { queryClient } from '../lib/queryClient';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
function pluginInfoKey(id: string, plugin: Plugin | null) {
|
||||
return ['plugin_info', id, plugin?.updatedAt ?? 'n/a'];
|
||||
function pluginInfoKey(id: string | null, plugin: Plugin | null) {
|
||||
return ['plugin_info', id ?? 'n/a', plugin?.updatedAt ?? 'n/a'];
|
||||
}
|
||||
|
||||
export function usePluginInfo(id: string) {
|
||||
export function usePluginInfo(id: string | null) {
|
||||
const plugins = useAtomValue(pluginsAtom);
|
||||
// Get the plugin so we can refetch whenever it's updated
|
||||
const plugin = plugins.find((p) => p.id === id);
|
||||
return useQuery({
|
||||
queryKey: pluginInfoKey(id, plugin ?? null),
|
||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||
queryFn: () => invokeCmd<PluginMetadata>('cmd_plugin_info', { id }),
|
||||
queryFn: () => {
|
||||
if (id == null) return null;
|
||||
return invokeCmd<PluginMetadata>('cmd_plugin_info', { id });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user