Add Coolify REST API server with Scalar docs and UI integration
Express API server on :3100 exposing all Coolify operations: - CRUD for apps, env vars, servers - Full upsert pipeline (create/update + env + route + deploy) - Drift detection, Traefik route management via SSH - Scalar API docs at /reference, OpenAPI 3.1 spec UI: New Coolify page with app cards, deploy/delete actions, env var expansion. Sidebar nav + React Query hooks + fetch client. Both UI and LLM/CLI use the same HTTP endpoints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { AppLayout } from '@/components/layout/app-layout';
|
||||
import { DashboardPage } from '@/components/dashboard/dashboard-page';
|
||||
import { ServersPage } from '@/components/servers/servers-page';
|
||||
import { ProjectsPage } from '@/components/projects/projects-page';
|
||||
import { CoolifyPage } from '@/components/coolify/coolify-page';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -25,6 +26,7 @@ const router = createHashRouter([
|
||||
{ path: 'servers', element: <ServersPage /> },
|
||||
{ path: 'projects', element: <ProjectsPage /> },
|
||||
{ path: 'projects/:projectName', element: <ProjectsPage /> },
|
||||
{ path: 'coolify', element: <CoolifyPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
92
app/renderer/src/components/coolify/coolify-app-card.jsx
Normal file
92
app/renderer/src/components/coolify/coolify-app-card.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useState } from 'react';
|
||||
import { Rocket, Trash2, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { StatusDot } from '@/components/shared/status-dot';
|
||||
|
||||
const buildpackColor = {
|
||||
dockercompose: 'secondary',
|
||||
nixpacks: 'outline',
|
||||
dockerfile: 'outline',
|
||||
};
|
||||
|
||||
function statusColor(status) {
|
||||
if (!status) return 'gray';
|
||||
const s = status.toLowerCase();
|
||||
if (s.includes('running') || s.includes('healthy')) return 'green';
|
||||
if (s.includes('stopped') || s.includes('exited')) return 'red';
|
||||
if (s.includes('building') || s.includes('starting')) return 'yellow';
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
export function CoolifyAppCard({ app, onDeploy, onDelete }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<StatusDot color={statusColor(app.status)} />
|
||||
{app.name}
|
||||
</CardTitle>
|
||||
<Badge variant={buildpackColor[app.build_pack] || 'outline'} className="text-[10px]">
|
||||
{app.build_pack}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-4 pt-0 space-y-2">
|
||||
{/* Port + domain */}
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
{app._host_port && <span>:{app._host_port}</span>}
|
||||
{app.git_branch && <span>{app.git_branch}</span>}
|
||||
{app.fqdn && (
|
||||
<a
|
||||
href={app.fqdn}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-primary hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{app.fqdn.replace(/^https?:\/\//, '')}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1" onClick={() => onDeploy(app)}>
|
||||
<Rocket className="h-3 w-3" /> Deploy
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1 text-destructive" onClick={() => onDelete(app)}>
|
||||
<Trash2 className="h-3 w-3" /> Delete
|
||||
</Button>
|
||||
<button
|
||||
className="ml-auto text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
||||
Envs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Expandable env vars */}
|
||||
{expanded && app._envs && (
|
||||
<div className="rounded-md bg-secondary/50 p-2 text-[11px] font-mono space-y-0.5 max-h-40 overflow-auto">
|
||||
{app._envs.length === 0 && <span className="text-muted-foreground">No env vars</span>}
|
||||
{app._envs.map((env) => (
|
||||
<div key={env.id || env.key} className="flex gap-2">
|
||||
<span className="text-primary">{env.key}</span>
|
||||
<span className="text-muted-foreground">=</span>
|
||||
<span className="truncate">{env.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
133
app/renderer/src/components/coolify/coolify-deploy-dialog.jsx
Normal file
133
app/renderer/src/components/coolify/coolify-deploy-dialog.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useState } from 'react';
|
||||
import { CheckCircle2, Circle, Loader2, XCircle, AlertTriangle, SkipForward } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const STEP_LABELS = {
|
||||
config: 'Read coolify.json',
|
||||
server: 'Resolve server',
|
||||
check: 'Check app existence',
|
||||
validate: 'Validate config',
|
||||
sync: 'Create / Update app',
|
||||
env: 'Set HOST_PORT',
|
||||
route: 'Configure routing',
|
||||
changelog: 'Write changelog',
|
||||
deploy: 'Trigger deployment',
|
||||
};
|
||||
|
||||
const statusIcon = {
|
||||
running: <Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />,
|
||||
done: <CheckCircle2 className="h-3.5 w-3.5 text-[hsl(180,80%,25%)]" />,
|
||||
error: <XCircle className="h-3.5 w-3.5 text-destructive" />,
|
||||
warn: <AlertTriangle className="h-3.5 w-3.5 text-[hsl(37,90%,58%)]" />,
|
||||
skipped: <SkipForward className="h-3.5 w-3.5 text-muted-foreground" />,
|
||||
};
|
||||
|
||||
export function CoolifyDeployDialog({ projectPath, appName, onClose }) {
|
||||
const [result, setResult] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [steps, setSteps] = useState([]);
|
||||
|
||||
const run = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
setSteps([]);
|
||||
try {
|
||||
const res = await fetch('/api/coolify/upsert', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projectPath, appName }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
||||
setSteps(data.steps || []);
|
||||
if (data.success) {
|
||||
setResult(data);
|
||||
} else {
|
||||
setError(data.error || 'Upsert failed');
|
||||
setSteps(data.steps || []);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center">
|
||||
<div className="bg-card border rounded-lg shadow-lg w-[520px] max-h-[80vh] overflow-auto">
|
||||
<div className="p-4 border-b">
|
||||
<h2 className="text-sm font-semibold">Deploy to Coolify</h2>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 truncate">{projectPath}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
{/* Not started */}
|
||||
{!loading && !result && !error && steps.length === 0 && (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
This will read <code className="bg-secondary px-1 rounded text-xs">coolify.json</code>,
|
||||
create/update the Coolify app, set env vars, configure Traefik routing, and trigger a deploy.
|
||||
</p>
|
||||
<Button onClick={run}>Start Deploy</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Steps */}
|
||||
{steps.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
{steps.map((s) => (
|
||||
<div key={s.step} className="flex items-start gap-2">
|
||||
<span className="mt-0.5 shrink-0">{statusIcon[s.status] || <Circle className="h-3.5 w-3.5 text-muted-foreground" />}</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-medium">{STEP_LABELS[s.step] || s.step}</p>
|
||||
{s.detail && (
|
||||
<p className="text-[11px] text-muted-foreground whitespace-pre-wrap break-all mt-0.5">{s.detail}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading spinner */}
|
||||
{loading && steps.length === 0 && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground py-4 justify-center">
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> Running upsert pipeline...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success */}
|
||||
{result && (
|
||||
<div className="rounded-md bg-[hsl(180,80%,25%)]/10 border border-[hsl(180,80%,25%)]/30 p-3 text-xs">
|
||||
<p className="font-medium text-[hsl(180,80%,25%)]">Deployment triggered</p>
|
||||
<p className="text-muted-foreground mt-1">UUID: {result.uuid}</p>
|
||||
{result.domain && (
|
||||
<a href={`https://${result.domain}`} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline mt-1 block">
|
||||
https://{result.domain}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 border border-destructive/30 p-3 text-xs">
|
||||
<p className="font-medium text-destructive">Error</p>
|
||||
<p className="text-muted-foreground mt-1 whitespace-pre-wrap">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t flex justify-end gap-2">
|
||||
{error && !loading && (
|
||||
<Button variant="secondary" size="sm" onClick={run}>Retry</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>Close</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
131
app/renderer/src/components/coolify/coolify-page.jsx
Normal file
131
app/renderer/src/components/coolify/coolify-page.jsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useState } from 'react';
|
||||
import { Cloud, RefreshCw, Rocket, ExternalLink } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { EmptyState } from '@/components/shared/empty-state';
|
||||
import { CoolifyAppCard } from './coolify-app-card';
|
||||
import { CoolifyDeployDialog } from './coolify-deploy-dialog';
|
||||
import { useCoolifyApps, useCoolifyDeploy, useCoolifyDelete, useCoolifyNextPort } from '@/hooks/use-coolify';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function CoolifyPage() {
|
||||
const { data: apps, isLoading, error, refetch } = useCoolifyApps();
|
||||
const { data: portInfo } = useCoolifyNextPort();
|
||||
const deployMut = useCoolifyDeploy();
|
||||
const deleteMut = useCoolifyDelete();
|
||||
|
||||
const [deployDialog, setDeployDialog] = useState(null); // { projectPath, appName }
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(null); // { uuid, name }
|
||||
|
||||
const handleDeploy = (app) => {
|
||||
deployMut.mutate(app.uuid, {
|
||||
onSuccess: () => toast.success(`Deploy triggered for ${app.name}`),
|
||||
onError: (err) => toast.error(`Deploy failed: ${err.message}`),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (app) => {
|
||||
setDeleteConfirm({ uuid: app.uuid, name: app.name });
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (!deleteConfirm) return;
|
||||
deleteMut.mutate(deleteConfirm.uuid, {
|
||||
onSuccess: () => {
|
||||
toast.success(`${deleteConfirm.name} deleted`);
|
||||
setDeleteConfirm(null);
|
||||
},
|
||||
onError: (err) => toast.error(`Delete failed: ${err.message}`),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold flex items-center gap-2">
|
||||
<Cloud className="h-6 w-6 text-primary" /> Coolify
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{apps ? `${apps.length} apps` : 'Loading...'}
|
||||
{portInfo && <span className="ml-2">Next port: {portInfo.nextPort}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href="http://localhost:3100/reference"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
|
||||
>
|
||||
API Docs <ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
<Button variant="ghost" size="sm" onClick={() => refetch()} disabled={isLoading}>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 border border-destructive/30 p-3 text-xs">
|
||||
<p className="font-medium text-destructive">Failed to load apps</p>
|
||||
<p className="text-muted-foreground mt-1">{error.message}</p>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Make sure the API server is running: <code className="bg-secondary px-1 rounded">npm run api</code>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete confirmation */}
|
||||
{deleteConfirm && (
|
||||
<div className="rounded-md bg-destructive/10 border border-destructive/30 p-3 flex items-center gap-3">
|
||||
<p className="text-xs flex-1">
|
||||
Delete <strong>{deleteConfirm.name}</strong> from Coolify? This cannot be undone.
|
||||
</p>
|
||||
<Button variant="destructive" size="sm" className="text-xs" onClick={confirmDelete} disabled={deleteMut.isPending}>
|
||||
{deleteMut.isPending ? 'Deleting...' : 'Yes, delete'}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="text-xs" onClick={() => setDeleteConfirm(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Apps grid */}
|
||||
{!error && apps && apps.length === 0 && (
|
||||
<EmptyState
|
||||
icon={Cloud}
|
||||
title="No Coolify apps"
|
||||
description="Create an app through the API or upsert from a project's coolify.json"
|
||||
/>
|
||||
)}
|
||||
|
||||
{apps && apps.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{apps.map((app) => (
|
||||
<CoolifyAppCard
|
||||
key={app.uuid}
|
||||
app={app}
|
||||
onDeploy={handleDeploy}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deploy dialog */}
|
||||
{deployDialog && (
|
||||
<CoolifyDeployDialog
|
||||
projectPath={deployDialog.projectPath}
|
||||
appName={deployDialog.appName}
|
||||
onClose={() => {
|
||||
setDeployDialog(null);
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { LayoutDashboard, Container, Settings } from 'lucide-react';
|
||||
import { LayoutDashboard, Container, Settings, Cloud } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useServers } from '@/hooks/use-servers';
|
||||
@@ -8,6 +8,7 @@ import { useAppContext } from '@/lib/app-context';
|
||||
const navItems = [
|
||||
{ to: '/', icon: LayoutDashboard, label: 'Dashboard', end: true },
|
||||
{ to: '/projects', icon: Container, label: 'Projects' },
|
||||
{ to: '/coolify', icon: Cloud, label: 'Coolify' },
|
||||
{ to: '/servers', icon: Settings, label: 'Settings' },
|
||||
];
|
||||
|
||||
|
||||
54
app/renderer/src/hooks/use-coolify.js
Normal file
54
app/renderer/src/hooks/use-coolify.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { coolifyApi } from '@/lib/api';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
|
||||
export function useCoolifyApps() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.coolify.apps,
|
||||
queryFn: () => coolifyApi.listApps(),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCoolifyServers() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.coolify.servers,
|
||||
queryFn: () => coolifyApi.listServers(),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCoolifyNextPort() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.coolify.nextPort,
|
||||
queryFn: () => coolifyApi.getNextPort(),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCoolifyDeploy() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (uuid) => coolifyApi.deploy(uuid),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.coolify.apps }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCoolifyUpsert() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ projectPath, appName }) => coolifyApi.upsert(projectPath, appName),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: queryKeys.coolify.apps });
|
||||
qc.invalidateQueries({ queryKey: queryKeys.coolify.nextPort });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCoolifyDelete() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (uuid) => coolifyApi.deleteApp(uuid),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.coolify.apps }),
|
||||
});
|
||||
}
|
||||
@@ -36,3 +36,36 @@ export const configApi = {
|
||||
export const toolsApi = {
|
||||
openVSCodeDiff: (data) => api.openVSCodeDiff(data),
|
||||
};
|
||||
|
||||
// ─── Coolify API (HTTP fetch to Express server) ────────────────────
|
||||
|
||||
const COOLIFY_BASE = '/api/coolify';
|
||||
|
||||
async function fetchJson(url, options) {
|
||||
const res = await fetch(url, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const coolifyApi = {
|
||||
listApps: () => fetchJson(`${COOLIFY_BASE}/apps`),
|
||||
findApp: (name) => fetchJson(`${COOLIFY_BASE}/apps/find/${encodeURIComponent(name)}`),
|
||||
createApp: (data) => fetchJson(`${COOLIFY_BASE}/apps`, { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateApp: (uuid, data) => fetchJson(`${COOLIFY_BASE}/apps/${uuid}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deleteApp: (uuid) => fetchJson(`${COOLIFY_BASE}/apps/${uuid}`, { method: 'DELETE' }),
|
||||
listEnvs: (uuid) => fetchJson(`${COOLIFY_BASE}/apps/${uuid}/envs`),
|
||||
setEnv: (uuid, key, value) => fetchJson(`${COOLIFY_BASE}/apps/${uuid}/envs`, { method: 'POST', body: JSON.stringify({ key, value }) }),
|
||||
deploy: (uuid) => fetchJson(`${COOLIFY_BASE}/apps/${uuid}/deploy`, { method: 'POST' }),
|
||||
listServers: () => fetchJson(`${COOLIFY_BASE}/servers`),
|
||||
getNextPort: () => fetchJson(`${COOLIFY_BASE}/next-port`),
|
||||
addRoute: (routeName, domain, port) => fetchJson(`${COOLIFY_BASE}/routes`, { method: 'POST', body: JSON.stringify({ routeName, domain, port }) }),
|
||||
checkDrift: (projectPath, appName) => fetchJson(`${COOLIFY_BASE}/drift`, { method: 'POST', body: JSON.stringify({ projectPath, appName }) }),
|
||||
upsert: (projectPath, appName) => fetchJson(`${COOLIFY_BASE}/upsert`, { method: 'POST', body: JSON.stringify({ projectPath, appName }) }),
|
||||
readConfig: (projectPath) => fetchJson(`${COOLIFY_BASE}/config?path=${encodeURIComponent(projectPath)}`),
|
||||
};
|
||||
|
||||
@@ -13,4 +13,10 @@ export const queryKeys = {
|
||||
config: {
|
||||
all: ['config'],
|
||||
},
|
||||
coolify: {
|
||||
apps: ['coolify', 'apps'],
|
||||
servers: ['coolify', 'servers'],
|
||||
nextPort: ['coolify', 'nextPort'],
|
||||
drift: (projectPath) => ['coolify', 'drift', projectPath],
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user