Files
dbx/apps/desktop/src/components/plugins/PluginWorkbenchTab.vue
T

108 lines
4.6 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { AlertTriangle, Loader2 } from "@lucide/vue";
import PluginWorkbenchHost from "@/components/plugins/PluginWorkbenchHost.vue";
import PluginRefreshState from "@/components/plugins/PluginRefreshState.vue";
import * as api from "@/lib/backend/api";
import { createFrontendPluginRegistry } from "@/lib/plugins/frontendPlugin";
import type { PluginWorkbenchContext } from "@/lib/plugins/pluginHostBridge";
import type { InstalledPlugin } from "@/types/database";
import { useQueryStore } from "@/stores/queryStore";
import { useI18n } from "vue-i18n";
import { useConnectionStore } from "@/stores/connectionStore";
const props = defineProps<{
pluginId: string;
contributionId: string;
connectionId?: string;
context?: PluginWorkbenchContext;
}>();
const { t, locale: appLocale } = useI18n();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const plugins = ref<InstalledPlugin[]>([]);
const loading = ref(true);
const error = ref("");
const deferred = ref(false);
let loadGeneration = 0;
const resolvedConnectionId = computed(() => props.connectionId || (typeof props.context?.connectionId === "string" ? props.context.connectionId : undefined));
const entry = computed(() => createFrontendPluginRegistry(plugins.value, appLocale.value).findWorkbench(props.pluginId, props.contributionId));
async function load() {
const generation = ++loadGeneration;
loading.value = true;
error.value = "";
try {
const installed = await api.listPlugins();
if (generation !== loadGeneration) return;
plugins.value = installed;
if (!entry.value) throw new Error(t("pluginPlatform.workbenchUnavailable", { pluginId: props.pluginId, contributionId: props.contributionId }));
} catch (cause) {
if (generation !== loadGeneration) return;
error.value = cause instanceof Error ? cause.message : String(cause);
} finally {
if (generation === loadGeneration) loading.value = false;
}
}
async function refresh() {
deferred.value = false;
loading.value = true;
error.value = "";
try {
if (resolvedConnectionId.value) await connectionStore.ensureConnected(resolvedConnectionId.value);
await load();
} catch (cause) {
loading.value = false;
error.value = cause instanceof Error ? cause.message : String(cause);
}
}
function start() {
const connectionId = resolvedConnectionId.value;
deferred.value = !!connectionId && !connectionStore.connectedIds.has(connectionId);
if (deferred.value) {
loading.value = false;
error.value = "";
return;
}
void load();
}
function openWorkbench(pluginId: string, contributionId: string, context?: PluginWorkbenchContext) {
const target = createFrontendPluginRegistry(plugins.value, appLocale.value).findWorkbench(pluginId, contributionId);
queryStore.openPluginWorkbench(pluginId, contributionId, { title: target?.contribution.label || contributionId, context });
}
function openFilesystem(pluginId: string, providerId: string, context?: PluginWorkbenchContext) {
const target = createFrontendPluginRegistry(plugins.value, appLocale.value)
.listFilesystemProviders()
.find((entry) => entry.plugin.manifest.id === pluginId && entry.contribution.id === providerId);
if (!target) throw new Error(t("pluginPlatform.filesystemUnavailable", { pluginId, providerId }));
queryStore.openPluginFilesystem(pluginId, providerId, {
title: target.contribution.label,
connectionId: typeof context?.connectionId === "string" ? context.connectionId : undefined,
rootUri: target.contribution.root_uri,
currentUri: typeof context?.uri === "string" ? context.uri : undefined,
});
}
onMounted(start);
watch(() => [props.pluginId, props.contributionId, props.connectionId, props.context?.connectionId], start);
defineExpose({ refresh });
</script>
<template>
<div class="flex size-full min-h-0">
<PluginRefreshState v-if="deferred" @refresh="void refresh()" />
<div v-else-if="loading" class="m-auto flex items-center text-sm text-muted-foreground"><Loader2 class="mr-2 size-4 animate-spin" />{{ t("pluginPlatform.loadingWorkbench") }}</div>
<div v-else-if="error || !entry" class="m-auto flex max-w-lg items-start gap-3 rounded-lg border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive">
<AlertTriangle class="mt-0.5 size-4 shrink-0" />
<span>{{ error || t("pluginPlatform.workbenchUnavailableFallback") }}</span>
</div>
<PluginWorkbenchHost v-else class="min-h-0 flex-1" :plugin="entry.plugin" :contribution="entry.contribution" :context="context" @open-workbench="openWorkbench" @open-filesystem="openFilesystem" />
</div>
</template>