mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-07 17:43:28 +01:00
Merge feature/task-007d-launcher-ui into cline-dev
TASK-007D: Local Backends UI Integration - useLocalBackends hook for IPC communication - LocalBackendCard component for backend management - Enhanced BackendServicesPanel with Local Backends section
This commit is contained in:
@@ -26,6 +26,8 @@ import { Text, TextType } from '@noodl-core-ui/components/typography/Text';
|
|||||||
|
|
||||||
import { AddBackendDialog } from './AddBackendDialog/AddBackendDialog';
|
import { AddBackendDialog } from './AddBackendDialog/AddBackendDialog';
|
||||||
import { BackendCard } from './BackendCard/BackendCard';
|
import { BackendCard } from './BackendCard/BackendCard';
|
||||||
|
import { useLocalBackends } from './hooks/useLocalBackends';
|
||||||
|
import { LocalBackendCard } from './LocalBackendCard/LocalBackendCard';
|
||||||
|
|
||||||
export function BackendServicesPanel() {
|
export function BackendServicesPanel() {
|
||||||
const [backends, setBackends] = useState(BackendServices.instance.backends);
|
const [backends, setBackends] = useState(BackendServices.instance.backends);
|
||||||
@@ -34,6 +36,16 @@ export function BackendServicesPanel() {
|
|||||||
const [isAddDialogVisible, setIsAddDialogVisible] = useState(false);
|
const [isAddDialogVisible, setIsAddDialogVisible] = useState(false);
|
||||||
const [hasActivity, setHasActivity] = useState(false);
|
const [hasActivity, setHasActivity] = useState(false);
|
||||||
|
|
||||||
|
// Local backends hook
|
||||||
|
const {
|
||||||
|
backends: localBackends,
|
||||||
|
isLoading: isLoadingLocal,
|
||||||
|
createBackend: createLocalBackend,
|
||||||
|
deleteBackend: deleteLocalBackend,
|
||||||
|
startBackend: startLocalBackend,
|
||||||
|
stopBackend: stopLocalBackend
|
||||||
|
} = useLocalBackends();
|
||||||
|
|
||||||
// Initialize backends on mount
|
// Initialize backends on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -113,14 +125,61 @@ export function BackendServicesPanel() {
|
|||||||
<BasePanel title="Backend Services" hasActivityBlocker={hasActivity} hasContentScroll>
|
<BasePanel title="Backend Services" hasActivityBlocker={hasActivity} hasContentScroll>
|
||||||
<DeleteDialog />
|
<DeleteDialog />
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading || isLoadingLocal ? (
|
||||||
<Container hasLeftSpacing hasTopSpacing>
|
<Container hasLeftSpacing hasTopSpacing>
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
</Container>
|
</Container>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* Local Backends Section */}
|
||||||
<Section
|
<Section
|
||||||
title="Available Backends"
|
title="Local Backends"
|
||||||
|
variant={SectionVariant.Panel}
|
||||||
|
actions={
|
||||||
|
<IconButton
|
||||||
|
icon={IconName.Plus}
|
||||||
|
size={IconSize.Small}
|
||||||
|
onClick={async () => {
|
||||||
|
const name = prompt('Enter backend name:');
|
||||||
|
if (name) {
|
||||||
|
await createLocalBackend(name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
testId="add-local-backend-button"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{localBackends.length > 0 ? (
|
||||||
|
<VStack>
|
||||||
|
{localBackends.map((backend) => (
|
||||||
|
<LocalBackendCard
|
||||||
|
key={backend.id}
|
||||||
|
backend={backend}
|
||||||
|
onStart={async () => startLocalBackend(backend.id)}
|
||||||
|
onStop={async () => stopLocalBackend(backend.id)}
|
||||||
|
onDelete={() => {
|
||||||
|
if (confirm('Delete this local backend? All data will be lost.')) {
|
||||||
|
deleteLocalBackend(backend.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<Container hasLeftSpacing hasTopSpacing hasBottomSpacing>
|
||||||
|
<Text>No local backends</Text>
|
||||||
|
<Box hasTopSpacing>
|
||||||
|
<Text textType={TextType.Shy}>
|
||||||
|
Create a local SQLite backend for zero-config database development.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* External Backends Section */}
|
||||||
|
<Section
|
||||||
|
title="External Backends"
|
||||||
variant={SectionVariant.Panel}
|
variant={SectionVariant.Panel}
|
||||||
actions={
|
actions={
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -147,7 +206,7 @@ export function BackendServicesPanel() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
) : (
|
) : (
|
||||||
<Container hasLeftSpacing hasTopSpacing hasBottomSpacing>
|
<Container hasLeftSpacing hasTopSpacing hasBottomSpacing>
|
||||||
<Text>No backends configured</Text>
|
<Text>No external backends configured</Text>
|
||||||
<Box hasTopSpacing>
|
<Box hasTopSpacing>
|
||||||
<Text textType={TextType.Shy}>
|
<Text textType={TextType.Shy}>
|
||||||
Click the + button to add a Directus, Supabase, or custom REST backend.
|
Click the + button to add a Directus, Supabase, or custom REST backend.
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
.Root {
|
||||||
|
background-color: var(--theme-color-bg-3);
|
||||||
|
border: 1px solid var(--theme-color-border-default);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--theme-color-border-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TypeIcon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--theme-color-success);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.StatusBadge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--theme-color-bg-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Endpoint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--theme-color-bg-2);
|
||||||
|
border: 1px solid var(--theme-color-border-default);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--theme-color-bg-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Info {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--theme-color-border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Actions {
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--theme-color-border-default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* LocalBackendCard
|
||||||
|
*
|
||||||
|
* Card component for displaying and managing a local SQLite backend.
|
||||||
|
* Shows status, start/stop controls, and endpoint information.
|
||||||
|
*
|
||||||
|
* @module BackendServicesPanel/LocalBackendCard
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
|
||||||
|
import { IconButton } from '@noodl-core-ui/components/inputs/IconButton';
|
||||||
|
import { PrimaryButton, PrimaryButtonSize, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
|
||||||
|
import { HStack, VStack } from '@noodl-core-ui/components/layout/Stack';
|
||||||
|
import { Text, TextType } from '@noodl-core-ui/components/typography/Text';
|
||||||
|
|
||||||
|
import { LocalBackendInfo } from '../hooks/useLocalBackends';
|
||||||
|
import css from './LocalBackendCard.module.scss';
|
||||||
|
|
||||||
|
export interface LocalBackendCardProps {
|
||||||
|
/** Backend information */
|
||||||
|
backend: LocalBackendInfo;
|
||||||
|
/** Called when start is requested */
|
||||||
|
onStart: () => Promise<void> | Promise<boolean>;
|
||||||
|
/** Called when stop is requested */
|
||||||
|
onStop: () => Promise<void> | Promise<boolean>;
|
||||||
|
/** Called when delete is requested */
|
||||||
|
onDelete: () => void;
|
||||||
|
/** Called when export is requested */
|
||||||
|
onExport?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status icon and color based on running status
|
||||||
|
*/
|
||||||
|
function getStatusDisplay(running: boolean): { icon: IconName; color: string; text: string } {
|
||||||
|
if (running) {
|
||||||
|
return { icon: IconName.Check, color: 'var(--theme-color-success)', text: 'Running' };
|
||||||
|
}
|
||||||
|
return { icon: IconName.CircleOpen, color: 'var(--theme-color-fg-default-shy)', text: 'Stopped' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalBackendCard({ backend, onStart, onStop, onDelete, onExport }: LocalBackendCardProps) {
|
||||||
|
const [isOperating, setIsOperating] = useState(false);
|
||||||
|
const statusDisplay = getStatusDisplay(backend.running);
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
const createdDate = new Date(backend.createdAt).toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle start/stop
|
||||||
|
const handleToggle = useCallback(async () => {
|
||||||
|
setIsOperating(true);
|
||||||
|
try {
|
||||||
|
if (backend.running) {
|
||||||
|
await onStop();
|
||||||
|
} else {
|
||||||
|
await onStart();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsOperating(false);
|
||||||
|
}
|
||||||
|
}, [backend.running, onStart, onStop]);
|
||||||
|
|
||||||
|
// Copy endpoint to clipboard
|
||||||
|
const handleCopyEndpoint = useCallback(() => {
|
||||||
|
if (backend.endpoint) {
|
||||||
|
navigator.clipboard.writeText(backend.endpoint);
|
||||||
|
}
|
||||||
|
}, [backend.endpoint]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={css.Root} data-test={`local-backend-card-${backend.id}`}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className={css.Header}>
|
||||||
|
<HStack hasSpacing>
|
||||||
|
<div className={css.TypeIcon}>
|
||||||
|
<Text textType={TextType.Proud}>L</Text>
|
||||||
|
</div>
|
||||||
|
<VStack>
|
||||||
|
<Text textType={TextType.DefaultContrast}>{backend.name}</Text>
|
||||||
|
<Text textType={TextType.Shy} style={{ fontSize: '11px' }}>
|
||||||
|
Local SQLite • Port {backend.port}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<div className={css.StatusBadge} style={{ color: statusDisplay.color }}>
|
||||||
|
<Icon icon={statusDisplay.icon} size={IconSize.Tiny} UNSAFE_style={{ color: statusDisplay.color }} />
|
||||||
|
<Text textType={TextType.Shy} style={{ fontSize: '10px', marginLeft: '4px' }}>
|
||||||
|
{statusDisplay.text}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Endpoint (when running) */}
|
||||||
|
{backend.running && backend.endpoint && (
|
||||||
|
<div className={css.Endpoint} onClick={handleCopyEndpoint}>
|
||||||
|
<Text textType={TextType.Shy} style={{ fontSize: '11px' }}>
|
||||||
|
{backend.endpoint}
|
||||||
|
</Text>
|
||||||
|
<Text textType={TextType.Shy} style={{ fontSize: '10px', marginLeft: '8px' }}>
|
||||||
|
(click to copy)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className={css.Info}>
|
||||||
|
<Text textType={TextType.Shy} style={{ fontSize: '11px' }}>
|
||||||
|
Created {createdDate}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className={css.Actions}>
|
||||||
|
<HStack hasSpacing>
|
||||||
|
<PrimaryButton
|
||||||
|
label={isOperating ? 'Processing...' : backend.running ? 'Stop' : 'Start'}
|
||||||
|
size={PrimaryButtonSize.Small}
|
||||||
|
variant={backend.running ? PrimaryButtonVariant.Muted : PrimaryButtonVariant.Muted}
|
||||||
|
onClick={handleToggle}
|
||||||
|
isDisabled={isOperating}
|
||||||
|
/>
|
||||||
|
{onExport && backend.running && (
|
||||||
|
<PrimaryButton
|
||||||
|
label="Export"
|
||||||
|
size={PrimaryButtonSize.Small}
|
||||||
|
variant={PrimaryButtonVariant.Muted}
|
||||||
|
onClick={onExport}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
icon={IconName.Trash}
|
||||||
|
size={IconSize.Small}
|
||||||
|
onClick={onDelete}
|
||||||
|
isDisabled={backend.running}
|
||||||
|
testId={`delete-local-backend-${backend.id}`}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* useLocalBackends
|
||||||
|
*
|
||||||
|
* React hook for managing local SQLite backends via IPC.
|
||||||
|
* Provides state and actions for the BYOB local backend system.
|
||||||
|
*
|
||||||
|
* @module BackendServicesPanel/hooks/useLocalBackends
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { platform } from '@noodl/platform';
|
||||||
|
|
||||||
|
/** Backend metadata as stored in config.json */
|
||||||
|
export interface LocalBackendMetadata {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
port: number;
|
||||||
|
projectIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extended backend info with runtime status */
|
||||||
|
export interface LocalBackendInfo extends LocalBackendMetadata {
|
||||||
|
running: boolean;
|
||||||
|
endpoint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook return type */
|
||||||
|
export interface UseLocalBackendsReturn {
|
||||||
|
/** List of all local backends */
|
||||||
|
backends: LocalBackendInfo[];
|
||||||
|
/** Whether initial loading is in progress */
|
||||||
|
isLoading: boolean;
|
||||||
|
/** Whether an operation is in progress */
|
||||||
|
isOperating: boolean;
|
||||||
|
/** Any error message */
|
||||||
|
error: string | null;
|
||||||
|
/** Refresh the backends list */
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
/** Create a new backend */
|
||||||
|
createBackend: (name: string) => Promise<LocalBackendMetadata | null>;
|
||||||
|
/** Delete a backend */
|
||||||
|
deleteBackend: (id: string) => Promise<boolean>;
|
||||||
|
/** Start a backend */
|
||||||
|
startBackend: (id: string) => Promise<boolean>;
|
||||||
|
/** Stop a backend */
|
||||||
|
stopBackend: (id: string) => Promise<boolean>;
|
||||||
|
/** Export schema */
|
||||||
|
exportSchema: (id: string, format: 'postgres' | 'supabase' | 'json') => Promise<string | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke IPC handler with error handling
|
||||||
|
*/
|
||||||
|
async function invokeIPC<T>(channel: string, ...args: unknown[]): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
// Use platform's invoke if available, otherwise fallback to window.electronAPI
|
||||||
|
const electronAPI = (window as unknown as { electronAPI?: { invoke: (ch: string, ...a: unknown[]) => Promise<T> } })
|
||||||
|
.electronAPI;
|
||||||
|
if (electronAPI?.invoke) {
|
||||||
|
return await electronAPI.invoke(channel, ...args);
|
||||||
|
}
|
||||||
|
// Try Noodl platform
|
||||||
|
if (platform && (platform as unknown as { ipcInvoke?: (ch: string, ...a: unknown[]) => Promise<T> }).ipcInvoke) {
|
||||||
|
return await (platform as unknown as { ipcInvoke: (ch: string, ...a: unknown[]) => Promise<T> }).ipcInvoke(
|
||||||
|
channel,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.warn('[useLocalBackends] No IPC transport available');
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[useLocalBackends] IPC error (${channel}):`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing local backends
|
||||||
|
*/
|
||||||
|
export function useLocalBackends(): UseLocalBackendsReturn {
|
||||||
|
const [backends, setBackends] = useState<LocalBackendInfo[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isOperating, setIsOperating] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Fetch all backends and their statuses
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const list = await invokeIPC<LocalBackendMetadata[]>('backend:list');
|
||||||
|
if (!list) {
|
||||||
|
setBackends([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status for each backend
|
||||||
|
const backendsWithStatus: LocalBackendInfo[] = await Promise.all(
|
||||||
|
list.map(async (backend) => {
|
||||||
|
const status = await invokeIPC<{ running: boolean; port?: number }>('backend:status', backend.id);
|
||||||
|
return {
|
||||||
|
...backend,
|
||||||
|
running: status?.running ?? false,
|
||||||
|
endpoint: status?.running && status?.port ? `http://localhost:${status.port}` : undefined
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setBackends(backendsWithStatus);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to fetch backends';
|
||||||
|
setError(message);
|
||||||
|
console.error('[useLocalBackends] refresh error:', err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
refresh().finally(() => setIsLoading(false));
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
// Create a new backend
|
||||||
|
const createBackend = useCallback(
|
||||||
|
async (name: string): Promise<LocalBackendMetadata | null> => {
|
||||||
|
setIsOperating(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await invokeIPC<LocalBackendMetadata>('backend:create', name);
|
||||||
|
await refresh();
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to create backend';
|
||||||
|
setError(message);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setIsOperating(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[refresh]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete a backend
|
||||||
|
const deleteBackend = useCallback(
|
||||||
|
async (id: string): Promise<boolean> => {
|
||||||
|
setIsOperating(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await invokeIPC<{ deleted: boolean }>('backend:delete', id);
|
||||||
|
await refresh();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to delete backend';
|
||||||
|
setError(message);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsOperating(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[refresh]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start a backend
|
||||||
|
const startBackend = useCallback(
|
||||||
|
async (id: string): Promise<boolean> => {
|
||||||
|
setIsOperating(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await invokeIPC<{ running: boolean }>('backend:start', id);
|
||||||
|
await refresh();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to start backend';
|
||||||
|
setError(message);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsOperating(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[refresh]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stop a backend
|
||||||
|
const stopBackend = useCallback(
|
||||||
|
async (id: string): Promise<boolean> => {
|
||||||
|
setIsOperating(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await invokeIPC<{ running: boolean }>('backend:stop', id);
|
||||||
|
await refresh();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to stop backend';
|
||||||
|
setError(message);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsOperating(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[refresh]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Export schema
|
||||||
|
const exportSchema = useCallback(
|
||||||
|
async (id: string, format: 'postgres' | 'supabase' | 'json'): Promise<string | null> => {
|
||||||
|
setIsOperating(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await invokeIPC<string>('backend:export-schema', id, format);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to export schema';
|
||||||
|
setError(message);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setIsOperating(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
backends,
|
||||||
|
isLoading,
|
||||||
|
isOperating,
|
||||||
|
error,
|
||||||
|
refresh,
|
||||||
|
createBackend,
|
||||||
|
deleteBackend,
|
||||||
|
startBackend,
|
||||||
|
stopBackend,
|
||||||
|
exportSchema
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user