API and Database Deployment Update
This commit is contained in:
@@ -5,11 +5,11 @@ import { token } from './auth';
|
||||
const API_BASE = "http://localhost:8080";
|
||||
|
||||
export interface DeployResponse {
|
||||
status: string;
|
||||
app_name: string;
|
||||
port: number;
|
||||
url: string;
|
||||
message: string;
|
||||
status: string;
|
||||
app_name: string;
|
||||
port: number;
|
||||
url: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
@@ -33,6 +33,20 @@ export interface Deployment {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Database {
|
||||
ID: number;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
DeletedAt: string | null;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
owner_id: string;
|
||||
size_mb: number;
|
||||
container_id: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -43,6 +57,9 @@ export interface Project {
|
||||
webhook_secret: string;
|
||||
git_token?: string;
|
||||
runtime?: string;
|
||||
build_command?: string;
|
||||
start_command?: string;
|
||||
install_command?: string;
|
||||
}
|
||||
|
||||
export async function getProject(id: string): Promise<Project | null> {
|
||||
@@ -126,6 +143,18 @@ export async function createProject(
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProject(id: string, data: Partial<Project>): Promise<Project | null> {
|
||||
try {
|
||||
return await fetchWithAuth(`/api/projects/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProjectEnv(id: string, envVars: Record<string, string>): Promise<boolean> {
|
||||
try {
|
||||
await fetchWithAuth(`/api/projects/${id}/env`, {
|
||||
@@ -139,10 +168,11 @@ export async function updateProjectEnv(id: string, envVars: Record<string, strin
|
||||
}
|
||||
}
|
||||
|
||||
export async function redeployProject(id: string): Promise<boolean> {
|
||||
export async function redeployProject(id: string, commit?: string): Promise<boolean> {
|
||||
try {
|
||||
await fetchWithAuth(`/api/projects/${id}/redeploy`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ commit }),
|
||||
});
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
@@ -240,6 +270,18 @@ export async function createDatabase(name: string, type: string = "sqlite") {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteDatabase(id: string) {
|
||||
try {
|
||||
await fetchWithAuth(`/api/storage/databases/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminUsers() {
|
||||
try {
|
||||
return await fetchWithAuth("/api/admin/users");
|
||||
@@ -269,3 +311,78 @@ export async function getAdminStats() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProfile() {
|
||||
try {
|
||||
return await fetchWithAuth("/api/user/");
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function regenerateAPIKey() {
|
||||
try {
|
||||
return await fetchWithAuth("/api/user/key", {
|
||||
method: "POST",
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDatabaseCredentials(id: string) {
|
||||
try {
|
||||
return await fetchWithAuth(`/api/storage/databases/${id}/credentials`);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDatabaseCredentials(id: string, username: string, password: string) {
|
||||
try {
|
||||
return await fetchWithAuth(`/api/storage/databases/${id}/credentials`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDatabase(id: string, port: number) {
|
||||
try {
|
||||
return await fetchWithAuth(`/api/storage/databases/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ port }),
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopDatabase(id: string) {
|
||||
try {
|
||||
return await fetchWithAuth(`/api/storage/databases/${id}/stop`, {
|
||||
method: "POST",
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartDatabase(id: string) {
|
||||
try {
|
||||
return await fetchWithAuth(`/api/storage/databases/${id}/restart`, {
|
||||
method: "POST",
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
16
frontend/src/lib/components/ui/tabs/index.ts
Normal file
16
frontend/src/lib/components/ui/tabs/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import Root from "./tabs.svelte";
|
||||
import Content from "./tabs-content.svelte";
|
||||
import List from "./tabs-list.svelte";
|
||||
import Trigger from "./tabs-trigger.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
List,
|
||||
Trigger,
|
||||
//
|
||||
Root as Tabs,
|
||||
Content as TabsContent,
|
||||
List as TabsList,
|
||||
Trigger as TabsTrigger,
|
||||
};
|
||||
17
frontend/src/lib/components/ui/tabs/tabs-content.svelte
Normal file
17
frontend/src/lib/components/ui/tabs/tabs-content.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.ContentProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="tabs-content"
|
||||
class={cn("flex-1 outline-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
frontend/src/lib/components/ui/tabs/tabs-list.svelte
Normal file
20
frontend/src/lib/components/ui/tabs/tabs-list.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.ListProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.List
|
||||
bind:ref
|
||||
data-slot="tabs-list"
|
||||
class={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
frontend/src/lib/components/ui/tabs/tabs-trigger.svelte
Normal file
20
frontend/src/lib/components/ui/tabs/tabs-trigger.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="tabs-trigger"
|
||||
class={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
19
frontend/src/lib/components/ui/tabs/tabs.svelte
Normal file
19
frontend/src/lib/components/ui/tabs/tabs.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="tabs"
|
||||
class={cn("flex flex-col gap-2", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -539,7 +539,7 @@
|
||||
</CardTitle>
|
||||
<CardDescription class="flex items-center gap-1" >
|
||||
<Github class="h-3 w-3" />
|
||||
<a href={project.repo_url} target="_blank">{new URL(project.repo_url).pathname.slice(1)}</a>
|
||||
<a class="underline" href={project.repo_url} target="_blank">{new URL(project.repo_url).pathname.slice(1)}</a>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<span
|
||||
@@ -592,7 +592,7 @@
|
||||
<span class="font-medium text-foreground truncate">
|
||||
{latestDeployment
|
||||
? new Date(
|
||||
latestDeployment.CreatedAt,
|
||||
latestDeployment.created_at,
|
||||
).toLocaleDateString()
|
||||
: "Never"}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { listProjects, type Project } from "$lib/api";
|
||||
import {
|
||||
listProjects,
|
||||
listDatabases,
|
||||
type Project,
|
||||
type Database,
|
||||
} from "$lib/api";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -23,14 +28,20 @@
|
||||
ExternalLink,
|
||||
Globe,
|
||||
Server,
|
||||
Database as DatabaseIcon,
|
||||
} from "@lucide/svelte";
|
||||
|
||||
let projects = $state<Project[]>([]);
|
||||
let databases = $state<Database[]>([]);
|
||||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
const res = await listProjects();
|
||||
if (res) projects = res;
|
||||
const [projRes, dbRes] = await Promise.all([
|
||||
listProjects(),
|
||||
listDatabases(),
|
||||
]);
|
||||
if (projRes) projects = projRes;
|
||||
if (dbRes) databases = dbRes;
|
||||
loading = false;
|
||||
});
|
||||
</script>
|
||||
@@ -55,18 +66,24 @@
|
||||
<CardHeader>
|
||||
<CardTitle>Active Services</CardTitle>
|
||||
<CardDescription>
|
||||
Overview of deployed applications and their internal/external ports.
|
||||
Overview of deployed applications and their
|
||||
internal/external ports.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if projects.length === 0}
|
||||
{#if projects.length === 0 && databases.length === 0}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-10 text-center"
|
||||
>
|
||||
<Network class="h-10 w-10 text-muted-foreground mb-4" />
|
||||
<h3 class="text-lg font-medium">No services found</h3>
|
||||
<Network
|
||||
class="h-10 w-10 text-muted-foreground mb-4"
|
||||
/>
|
||||
<h3 class="text-lg font-medium">
|
||||
No services found
|
||||
</h3>
|
||||
<p class="text-muted-foreground">
|
||||
Deploy a project to populate the network map.
|
||||
Deploy a project or create a database to
|
||||
populate the network map.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -78,15 +95,21 @@
|
||||
<TableHead>Port</TableHead>
|
||||
<TableHead>URL</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead class="text-right">Action</TableHead>
|
||||
<TableHead class="text-right"
|
||||
>Action</TableHead
|
||||
>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each projects as project}
|
||||
<TableRow>
|
||||
<TableCell class="font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<Server class="h-4 w-4 text-muted-foreground" />
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Server
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<a
|
||||
href={`/projects/${project.id}`}
|
||||
class="hover:underline"
|
||||
@@ -95,11 +118,15 @@
|
||||
</a>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="font-mono text-xs">localhost</TableCell>
|
||||
<TableCell class="font-mono text-xs"
|
||||
>localhost</TableCell
|
||||
>
|
||||
<TableCell class="font-mono text-xs"
|
||||
>{project.port}</TableCell
|
||||
>
|
||||
<TableCell class="font-mono text-xs text-muted-foreground">
|
||||
<TableCell
|
||||
class="font-mono text-xs text-muted-foreground"
|
||||
>
|
||||
http://localhost:{project.port}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -122,6 +149,47 @@
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
{#each databases as db}
|
||||
<TableRow>
|
||||
<TableCell class="font-medium">
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<DatabaseIcon
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<a
|
||||
href="/storage"
|
||||
class="hover:underline"
|
||||
>
|
||||
{db.name}
|
||||
</a>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="font-mono text-xs"
|
||||
>localhost</TableCell
|
||||
>
|
||||
<TableCell class="font-mono text-xs"
|
||||
>{db.port}</TableCell
|
||||
>
|
||||
<TableCell
|
||||
class="font-mono text-xs text-muted-foreground"
|
||||
>
|
||||
{db.type}://localhost:{db.port}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold
|
||||
bg-green-500/15 text-green-500 border-transparent capitalize"
|
||||
>
|
||||
{db.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<!-- No external link action for now -->
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { getProject, type Project } from "$lib/api";
|
||||
import { getProject, type Project, redeployProject } from "$lib/api";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -20,7 +20,10 @@
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Play,
|
||||
RotateCcw,
|
||||
} from "@lucide/svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
let project = $state<Project | null>(null);
|
||||
let loading = $state(true);
|
||||
@@ -69,6 +72,21 @@
|
||||
return AlertCircle;
|
||||
}
|
||||
}
|
||||
async function handleRedeploy(commit?: string) {
|
||||
if (!project) return;
|
||||
toast.info(commit ? `Redeploying commit ${commit.substring(0, 7)}...` : "Starting redeployment...");
|
||||
const success = await redeployProject(project.id.toString(), commit);
|
||||
if (success) {
|
||||
toast.success("Redeployment started!");
|
||||
// Refresh project data to show new deployment
|
||||
setTimeout(async () => {
|
||||
if (project) {
|
||||
const res = await getProject(project.id);
|
||||
if (res) project = res;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
@@ -84,6 +102,9 @@
|
||||
History of your application builds.
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => handleRedeploy()}>
|
||||
<Play class="mr-2 h-4 w-4" /> Redeploy
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card class="border-border/60">
|
||||
@@ -130,6 +151,7 @@
|
||||
<GitCommit class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span
|
||||
class="bg-muted px-2 py-0.5 rounded-md border border-border/50 text-foreground/80 font-mono"
|
||||
title={deployment.commit}
|
||||
>
|
||||
{deployment.commit === "HEAD"
|
||||
? "HEAD"
|
||||
@@ -178,6 +200,17 @@
|
||||
>
|
||||
<Terminal class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{#if deployment.commit !== "HEAD" && deployment.commit !== "MANUAL" && deployment.commit !== "WEBHOOK"}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
onclick={() => handleRedeploy(deployment.commit)}
|
||||
title="Redeploy this version"
|
||||
>
|
||||
<RotateCcw class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { getProject, type Project, updateProjectEnv } from "$lib/api";
|
||||
import {
|
||||
getProject,
|
||||
type Project,
|
||||
updateProjectEnv,
|
||||
updateProject,
|
||||
getSystemStatus,
|
||||
} from "$lib/api";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "$lib/components/ui/select";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -28,22 +40,138 @@
|
||||
let project = $state<Project | null>(null);
|
||||
let loading = $state(true);
|
||||
let showSecret = $state(false);
|
||||
let systemStatus = $state<{ local_ip: string; public_ip: string } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
let tempEnvVars = $state<{ key: string; value: string }[]>([]);
|
||||
let isDirty = $state(false);
|
||||
let formLoading = $state(false);
|
||||
|
||||
let projectName = $state("");
|
||||
let repoUrl = $state("");
|
||||
let gitToken = $state("");
|
||||
let runtime = $state("");
|
||||
let installCmd = $state("");
|
||||
let buildCmd = $state("");
|
||||
let startCmd = $state("");
|
||||
|
||||
const runtimeConfig: Record<
|
||||
string,
|
||||
{ install: string; build: string; start: string }
|
||||
> = {
|
||||
nodejs: {
|
||||
install: "npm install",
|
||||
build: "npm run build",
|
||||
start: "npm start",
|
||||
},
|
||||
bun: {
|
||||
install: "bun install",
|
||||
build: "bun run build",
|
||||
start: "bun start",
|
||||
},
|
||||
python: {
|
||||
install: "pip install -r requirements.txt",
|
||||
build: "",
|
||||
start: "python3 main.py",
|
||||
},
|
||||
go: {
|
||||
install: "go mod download",
|
||||
build: "go build -o main .",
|
||||
start: "./main",
|
||||
},
|
||||
rust: {
|
||||
install: "cargo fetch",
|
||||
build: "cargo build --release",
|
||||
start: "./target/release/main",
|
||||
},
|
||||
php: {
|
||||
install: "composer install",
|
||||
build: "",
|
||||
start: "php -S 0.0.0.0:8080",
|
||||
},
|
||||
java: {
|
||||
install: "mvn clean install",
|
||||
build: "mvn package",
|
||||
start: "java -jar target/app.jar",
|
||||
},
|
||||
static: {
|
||||
install: "",
|
||||
build: "",
|
||||
start: "",
|
||||
},
|
||||
dockerfile: {
|
||||
install: "",
|
||||
build: "",
|
||||
start: "",
|
||||
},
|
||||
};
|
||||
|
||||
let defaults = $derived(
|
||||
runtimeConfig[runtime] || {
|
||||
install: "npm install",
|
||||
build: "npm run build",
|
||||
start: "npm start",
|
||||
},
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
const id = $page.params.id;
|
||||
if (id) {
|
||||
const res = await getProject(id);
|
||||
if (res) {
|
||||
project = res;
|
||||
initEnvVars();
|
||||
}
|
||||
const [projRes, sysRes] = await Promise.all([
|
||||
id ? getProject(id) : null,
|
||||
getSystemStatus(),
|
||||
]);
|
||||
|
||||
if (projRes) {
|
||||
project = projRes;
|
||||
initFormData();
|
||||
initEnvVars();
|
||||
}
|
||||
|
||||
if (sysRes) {
|
||||
systemStatus = sysRes;
|
||||
}
|
||||
loading = false;
|
||||
});
|
||||
|
||||
function initFormData() {
|
||||
if (!project) return;
|
||||
projectName = project.name;
|
||||
repoUrl = project.repo_url;
|
||||
runtime = project.runtime || "nodejs";
|
||||
installCmd = project.install_command || "";
|
||||
buildCmd = project.build_command || "";
|
||||
startCmd = project.start_command || "";
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
if (!project) return;
|
||||
formLoading = true;
|
||||
const res = await updateProject(project.id, {
|
||||
name: projectName,
|
||||
repo_url: repoUrl,
|
||||
runtime,
|
||||
install_command: installCmd,
|
||||
build_command: buildCmd,
|
||||
start_command: startCmd,
|
||||
...(gitToken ? { git_token: gitToken } : {}),
|
||||
});
|
||||
formLoading = false;
|
||||
|
||||
if (res) {
|
||||
project = res;
|
||||
initFormData();
|
||||
toast.success("Settings updated successfully");
|
||||
}
|
||||
}
|
||||
|
||||
function copyId() {
|
||||
if (project) {
|
||||
navigator.clipboard.writeText(project.id);
|
||||
toast.success("Project ID copied");
|
||||
}
|
||||
}
|
||||
|
||||
function initEnvVars() {
|
||||
if (project?.env_vars) {
|
||||
tempEnvVars = project.env_vars.map((e) => ({
|
||||
@@ -139,7 +267,10 @@
|
||||
} else {
|
||||
tempEnvVars = [
|
||||
...tempEnvVars,
|
||||
{ key: key.trim(), value: value.replace(/^["'](.*)["']$/, "$1") },
|
||||
{
|
||||
key: key.trim(),
|
||||
value: value.replace(/^["'](.*)["']$/, "$1"),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -156,6 +287,196 @@
|
||||
</div>
|
||||
{:else if project}
|
||||
<div class="space-y-6 max-w-4xl">
|
||||
<Card class="border-border/60">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-xl font-bold"
|
||||
>Git Configuration</CardTitle
|
||||
>
|
||||
<CardDescription>
|
||||
Connect your project to a Git repository.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Project Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
bind:value={projectName}
|
||||
placeholder="my-awesome-project"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="repo">Repository URL</Label>
|
||||
<Input
|
||||
id="repo"
|
||||
bind:value={repoUrl}
|
||||
placeholder="https://github.com/user/repo"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="token">Git Token (Optional)</Label>
|
||||
<Input
|
||||
id="token"
|
||||
type="password"
|
||||
bind:value={gitToken}
|
||||
placeholder="Start typing to update..."
|
||||
/>
|
||||
<p class="text-[0.8rem] text-muted-foreground">
|
||||
Leave empty to keep the existing token.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="border-t px-4 flex justify-end">
|
||||
<Button onclick={saveSettings} disabled={formLoading} size="sm">
|
||||
{#if formLoading}
|
||||
<Loader2 class="h-4 w-4 mr-2 animate-spin" /> Saving...
|
||||
{:else}
|
||||
<Save class="h-4 w-4 mr-2" /> Save Changes
|
||||
{/if}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card class="border-border/60">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-xl font-bold"
|
||||
>Build & Output Settings</CardTitle
|
||||
>
|
||||
<CardDescription>
|
||||
Configure how your application is built and run.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="runtime">Runtime</Label>
|
||||
<Select type="single" bind:value={runtime}>
|
||||
<SelectTrigger class="w-full">
|
||||
{runtime
|
||||
? runtime.charAt(0).toUpperCase() +
|
||||
runtime.slice(1)
|
||||
: "Select a runtime"}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="nodejs">Node.js</SelectItem>
|
||||
<SelectItem value="bun">Bun</SelectItem>
|
||||
<SelectItem value="python">Python</SelectItem>
|
||||
<SelectItem value="go">Go</SelectItem>
|
||||
<SelectItem value="rust">Rust</SelectItem>
|
||||
<SelectItem value="php">PHP</SelectItem>
|
||||
<SelectItem value="java">Java</SelectItem>
|
||||
<SelectItem value="static">Static</SelectItem>
|
||||
<SelectItem value="dockerfile"
|
||||
>Dockerfile</SelectItem
|
||||
>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="install">Install Command</Label>
|
||||
<Input
|
||||
id="install"
|
||||
bind:value={installCmd}
|
||||
placeholder={defaults.install}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="build">Build Command</Label>
|
||||
<Input
|
||||
id="build"
|
||||
bind:value={buildCmd}
|
||||
placeholder={defaults.build}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="start">Start Command</Label>
|
||||
<Input
|
||||
id="start"
|
||||
bind:value={startCmd}
|
||||
placeholder={defaults.start}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="border-t px-4 flex justify-end">
|
||||
<Button onclick={saveSettings} disabled={formLoading} size="sm">
|
||||
{#if formLoading}
|
||||
<Loader2 class="h-4 w-4 mr-2 animate-spin" /> Saving...
|
||||
{:else}
|
||||
<Save class="h-4 w-4 mr-2" /> Save Changes
|
||||
{/if}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card class="border-border/60">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-xl font-bold">Networking</CardTitle>
|
||||
<CardDescription>
|
||||
Manage network settings for your deployment.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="port">Internal Port</Label>
|
||||
<Input
|
||||
id="port"
|
||||
value={project.port.toString()}
|
||||
readonly
|
||||
class="bg-muted font-mono"
|
||||
/>
|
||||
<p class="text-[0.8rem] text-muted-foreground">
|
||||
This port is assigned by the system and cannot be
|
||||
changed.
|
||||
</p>
|
||||
</div>
|
||||
{#if systemStatus}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
|
||||
<div class="grid gap-2">
|
||||
<Label>Local Network URL</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
readonly
|
||||
value={`http://${systemStatus.local_ip}:${project.port}`}
|
||||
class="bg-muted font-mono"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={() =>
|
||||
window.open(
|
||||
`http://${systemStatus!.local_ip}:${project!.port}`,
|
||||
"_blank",
|
||||
)}
|
||||
>
|
||||
<Eye class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Public Network URL</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
readonly
|
||||
value={`http://${systemStatus.public_ip}:${project.port}`}
|
||||
class="bg-muted font-mono"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={() =>
|
||||
window.open(
|
||||
`http://${systemStatus!.public_ip}:${project!.port}`,
|
||||
"_blank",
|
||||
)}
|
||||
>
|
||||
<Eye class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="border-border/60">
|
||||
<CardHeader class="pb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -163,11 +484,18 @@
|
||||
<CardTitle class="text-xl font-bold"
|
||||
>Environment Variables</CardTitle
|
||||
>
|
||||
<CardDescription class="mt-1 text-sm text-muted-foreground">
|
||||
<CardDescription
|
||||
class="mt-1 text-sm text-muted-foreground"
|
||||
>
|
||||
Configure runtime environment variables.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onclick={toggleSecret} class="h-8">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={toggleSecret}
|
||||
class="h-8"
|
||||
>
|
||||
{#if showSecret}
|
||||
<EyeOff class="h-4 w-4" />
|
||||
{:else}
|
||||
@@ -231,7 +559,11 @@
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="border-t px-4 flex justify-end">
|
||||
<Button onclick={saveEnvVars} disabled={!isDirty || loading} size="sm">
|
||||
<Button
|
||||
onclick={saveEnvVars}
|
||||
disabled={!isDirty || loading}
|
||||
size="sm"
|
||||
>
|
||||
{#if loading}
|
||||
<Loader2 class="h-4 w-4 mr-2 animate-spin" /> Saving...
|
||||
{:else}
|
||||
@@ -245,7 +577,8 @@
|
||||
<CardHeader>
|
||||
<CardTitle class="text-lg">Webhook Integration</CardTitle>
|
||||
<CardDescription class="mt-1.5">
|
||||
Trigger deployments automatically when you push to your repository.
|
||||
Trigger deployments automatically when you push to your
|
||||
repository.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
@@ -257,7 +590,11 @@
|
||||
value={`http://localhost:8080/projects/${project.id}/webhook/${project.webhook_secret}`}
|
||||
class="bg-muted font-mono text-xs"
|
||||
/>
|
||||
<Button variant="outline" size="icon" onclick={copyWebhook}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={copyWebhook}
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -267,7 +604,9 @@
|
||||
|
||||
<Card class="border-destructive/50 bg-destructive/5">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-destructive text-lg">Danger Zone</CardTitle>
|
||||
<CardTitle class="text-destructive text-lg"
|
||||
>Danger Zone</CardTitle
|
||||
>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
144
frontend/src/routes/settings/session/+page.svelte
Normal file
144
frontend/src/routes/settings/session/+page.svelte
Normal file
@@ -0,0 +1,144 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { getProfile, regenerateAPIKey } from "$lib/api";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
} from "$lib/components/ui/card";
|
||||
import { Loader2, Copy, RefreshCw, Terminal } from "@lucide/svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
let loading = $state(true);
|
||||
let apiKey = $state("");
|
||||
let regenerating = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
const profile = await getProfile();
|
||||
if (profile) {
|
||||
apiKey = profile.api_key;
|
||||
}
|
||||
loading = false;
|
||||
});
|
||||
|
||||
async function handleRegenerate() {
|
||||
if (
|
||||
!confirm("Are you sure? This will invalidate your current API key.")
|
||||
)
|
||||
return;
|
||||
|
||||
regenerating = true;
|
||||
const res = await regenerateAPIKey();
|
||||
regenerating = false;
|
||||
|
||||
if (res && res.api_key) {
|
||||
apiKey = res.api_key;
|
||||
toast.success("API Key regenerated successfully");
|
||||
}
|
||||
}
|
||||
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
toast.success("API Key copied to clipboard");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto py-10 px-4 max-w-4xl">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold tracking-tight">
|
||||
Session & API Access
|
||||
</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Manage your API keys for CLI and external access.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center p-10">
|
||||
<Loader2 class="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
{:else}
|
||||
<Card class="border-border/60">
|
||||
<CardHeader>
|
||||
<CardTitle>Personal Access Token</CardTitle>
|
||||
<CardDescription>
|
||||
Use this token to authenticate with the Clickploy CLI
|
||||
and API. Treat this token like a password.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Your API Key</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
readonly
|
||||
value={apiKey}
|
||||
class="font-mono bg-muted"
|
||||
type="password"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={copyKey}
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-muted/50 p-4 rounded-lg space-y-2 border border-border/50"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<Terminal class="h-4 w-4" />
|
||||
CLI Usage
|
||||
</div>
|
||||
<div
|
||||
class="text-xs font-mono bg-background p-2 rounded border border-border/50"
|
||||
>
|
||||
clickploy login --token {apiKey}
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Or use it in the Authorization header for API
|
||||
requests:
|
||||
</p>
|
||||
<div
|
||||
class="text-xs font-mono bg-background p-2 rounded border border-border/50"
|
||||
>
|
||||
Authorization: Bearer {apiKey}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter
|
||||
class="border-t px-6 py-4 flex justify-between items-center bg-muted/40"
|
||||
>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
Last generated: {new Date().toLocaleDateString()}
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onclick={handleRegenerate}
|
||||
disabled={regenerating}
|
||||
>
|
||||
{#if regenerating}
|
||||
<Loader2 class="h-3 w-3 mr-2 animate-spin" />
|
||||
{:else}
|
||||
<RefreshCw class="h-3 w-3 mr-2" />
|
||||
{/if}
|
||||
Regenerate Token
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,21 @@
|
||||
Plus,
|
||||
Server,
|
||||
AlertCircle,
|
||||
Trash,
|
||||
Copy,
|
||||
Activity,
|
||||
Calendar,
|
||||
Cloud,
|
||||
Box,
|
||||
Check,
|
||||
Power,
|
||||
Play,
|
||||
RotateCw,
|
||||
} from "@lucide/svelte";
|
||||
import * as Tabs from "$lib/components/ui/tabs";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -13,10 +27,23 @@
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
} from "$lib/components/ui/card";
|
||||
import { Progress } from "$lib/components/ui/progress";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
|
||||
import { getStorageStats, listDatabases, createDatabase } from "$lib/api";
|
||||
import {
|
||||
getStorageStats,
|
||||
listDatabases,
|
||||
createDatabase,
|
||||
deleteDatabase,
|
||||
getDatabaseCredentials,
|
||||
updateDatabaseCredentials,
|
||||
updateDatabase,
|
||||
stopDatabase,
|
||||
restartDatabase,
|
||||
type Database as DatabaseType,
|
||||
} from "$lib/api";
|
||||
import { onMount } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
@@ -26,27 +53,76 @@
|
||||
totalStorage > 0 ? (usedStorage / totalStorage) * 100 : 0,
|
||||
);
|
||||
|
||||
let userDatabases = $state<any[]>([]);
|
||||
let userDatabases = $state<DatabaseType[]>([]);
|
||||
let loading = $state(true);
|
||||
|
||||
// Credentials Dialog State (Creation)
|
||||
let showCredsDialog = $state(false);
|
||||
let newCreds = $state({
|
||||
uri: "",
|
||||
username: "",
|
||||
password: "",
|
||||
port: 0,
|
||||
name: "",
|
||||
host: "localhost",
|
||||
});
|
||||
|
||||
// Management Dialog State
|
||||
let showManageDialog = $state(false);
|
||||
let manageDb = $state<DatabaseType | null>(null);
|
||||
let manageCreds = $state({
|
||||
username: "",
|
||||
password: "",
|
||||
uri: "",
|
||||
public_uri: "",
|
||||
loading: true,
|
||||
port: 0,
|
||||
});
|
||||
let isUpdatingCreds = $state(false);
|
||||
let isPowerAction = $state(false);
|
||||
|
||||
// Creation Dialog State
|
||||
let showCreateDialog = $state(false);
|
||||
let selectedDbType = $state("");
|
||||
let newDbName = $state("");
|
||||
let isCreating = $state(false);
|
||||
|
||||
const availableTypes = [
|
||||
{
|
||||
name: "SQLite",
|
||||
description: "Embedded, serverless database engine.",
|
||||
type: "sqlite",
|
||||
status: "Available",
|
||||
icon: Box,
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-500/10",
|
||||
},
|
||||
{
|
||||
name: "MongoDB",
|
||||
description: "NoSQL document database.",
|
||||
type: "mongodb",
|
||||
status: "Available",
|
||||
icon: Database,
|
||||
color: "text-green-500",
|
||||
bgColor: "bg-green-500/10",
|
||||
},
|
||||
{
|
||||
name: "PostgreSQL",
|
||||
description: "Advanced open source relational database.",
|
||||
type: "postgres",
|
||||
status: "Coming Soon",
|
||||
icon: Cloud,
|
||||
color: "text-indigo-500",
|
||||
bgColor: "bg-indigo-500/10",
|
||||
},
|
||||
{
|
||||
name: "Redis",
|
||||
description: "In-memory data structure store.",
|
||||
type: "redis",
|
||||
status: "Coming Soon",
|
||||
icon: Activity,
|
||||
color: "text-red-500",
|
||||
bgColor: "bg-red-500/10",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -65,129 +141,693 @@
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function handleCreate(type: string) {
|
||||
const name = prompt("Enter database name:");
|
||||
if (!name) return;
|
||||
function initiateCreate(type: string) {
|
||||
selectedDbType = type;
|
||||
newDbName = "";
|
||||
showCreateDialog = true;
|
||||
}
|
||||
|
||||
const res = await createDatabase(name, type);
|
||||
if (res) {
|
||||
toast.success("Database created successfully!");
|
||||
async function performCreate() {
|
||||
if (!newDbName.trim()) return;
|
||||
isCreating = true;
|
||||
|
||||
try {
|
||||
const res: any = await createDatabase(newDbName, selectedDbType);
|
||||
showCreateDialog = false; // Close name input dialog
|
||||
|
||||
if (res) {
|
||||
toast.success("Database created successfully!");
|
||||
if (res.uri) {
|
||||
newCreds = {
|
||||
uri: res.uri,
|
||||
username: res.username,
|
||||
password: res.password,
|
||||
port: res.database.port,
|
||||
name: res.database.name,
|
||||
host: window.location.hostname,
|
||||
};
|
||||
// Handle localhost vs public IP (simplified logic)
|
||||
if (
|
||||
newCreds.host !== "localhost" &&
|
||||
newCreds.host !== "127.0.0.1"
|
||||
) {
|
||||
newCreds.uri = newCreds.uri.replace(
|
||||
"@localhost",
|
||||
`@${newCreds.host}`,
|
||||
);
|
||||
}
|
||||
showCredsDialog = true;
|
||||
}
|
||||
loadData();
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error("Failed to create database");
|
||||
console.error(e);
|
||||
} finally {
|
||||
isCreating = false;
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success("Copied to clipboard");
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
if (
|
||||
!confirm(
|
||||
"Are you sure you want to delete this database? This action cannot be undone.",
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
const success = await deleteDatabase(id.toString());
|
||||
if (success) {
|
||||
toast.success("Database deleted successfully!");
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCardClick(db: DatabaseType) {
|
||||
if (db.type !== "mongodb") return; // Only Mongo supported for now for rich management
|
||||
manageDb = db;
|
||||
showManageDialog = true;
|
||||
manageCreds = { ...manageCreds, loading: true };
|
||||
|
||||
const creds = await getDatabaseCredentials(db.ID.toString());
|
||||
if (creds) {
|
||||
manageCreds = {
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
uri: creds.uri,
|
||||
public_uri: creds.public_uri,
|
||||
loading: false,
|
||||
port: db.port,
|
||||
};
|
||||
} else {
|
||||
manageCreds.loading = false;
|
||||
toast.error("Failed to load credentials");
|
||||
}
|
||||
}
|
||||
|
||||
async function performUpdateCreds() {
|
||||
if (!manageDb) return;
|
||||
isUpdatingCreds = true;
|
||||
|
||||
try {
|
||||
// Update Port if changed
|
||||
if (manageCreds.port !== manageDb.port) {
|
||||
const res = await updateDatabase(
|
||||
manageDb.ID.toString(),
|
||||
Number(manageCreds.port),
|
||||
);
|
||||
if (res) {
|
||||
toast.success("Port updated successfully!");
|
||||
await loadData();
|
||||
manageDb =
|
||||
userDatabases.find((d) => d.ID === manageDb!.ID) ||
|
||||
manageDb;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Credentials
|
||||
const success = await updateDatabaseCredentials(
|
||||
manageDb.ID.toString(),
|
||||
manageCreds.username,
|
||||
manageCreds.password,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
toast.success("Credentials updated successfully!");
|
||||
const creds = await getDatabaseCredentials(
|
||||
manageDb.ID.toString(),
|
||||
);
|
||||
if (creds) {
|
||||
manageCreds.uri = creds.uri;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
isUpdatingCreds = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function performStopDatabase() {
|
||||
if (!manageDb) return;
|
||||
isPowerAction = true;
|
||||
const res = await stopDatabase(manageDb.ID.toString());
|
||||
if (res) {
|
||||
toast.success("Database stopped successfully");
|
||||
manageDb.status = "stopped";
|
||||
// Update the list
|
||||
userDatabases = userDatabases.map((d) =>
|
||||
d.ID === manageDb?.ID ? { ...d, status: "stopped" } : d,
|
||||
);
|
||||
}
|
||||
isPowerAction = false;
|
||||
}
|
||||
|
||||
async function performRestartDatabase() {
|
||||
if (!manageDb) return;
|
||||
isPowerAction = true;
|
||||
const res = await restartDatabase(manageDb.ID.toString());
|
||||
if (res) {
|
||||
toast.success("Database restarted successfully");
|
||||
manageDb.status = "running";
|
||||
// Update the list
|
||||
userDatabases = userDatabases.map((d) =>
|
||||
d.ID === manageDb?.ID ? { ...d, status: "running" } : d,
|
||||
);
|
||||
}
|
||||
isPowerAction = false;
|
||||
}
|
||||
|
||||
onMount(loadData);
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto py-10 px-4">
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div class="container mx-auto py-8 px-4 max-w-7xl">
|
||||
<div
|
||||
class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4"
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Storage</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Manage databases and view storage usage.
|
||||
<h1 class="text-3xl font-bold tracking-tight">Databases</h1>
|
||||
<p class="text-muted-foreground mt-1">
|
||||
Manage your database instances and storage volume.
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus class="mr-2 h-4 w-4" /> Create Database
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-3 mb-8">
|
||||
<Card class="md:col-span-3">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<HardDrive class="h-5 w-5" /> Storage Usage
|
||||
<div class="grid gap-6 md:grid-cols-3 mb-10">
|
||||
<Card
|
||||
class="md:col-span-3 overflow-hidden border-l-4 border-l-primary/50"
|
||||
>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="flex items-center gap-2 text-lg">
|
||||
<HardDrive class="h-5 w-5 text-primary" /> Storage Volume
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Total disk space used on the host machine.
|
||||
Total disk space usage across all your deployments and
|
||||
databases.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="font-medium"
|
||||
>{(usedStorage / 1024).toFixed(2)} GB used</span
|
||||
>
|
||||
<span class="text-muted-foreground"
|
||||
>{(totalStorage / 1024).toFixed(2)} GB total</span
|
||||
<div class="flex items-end justify-between">
|
||||
<div>
|
||||
<span class="text-2xl font-bold"
|
||||
>{(usedStorage / 1024).toFixed(2)} GB</span
|
||||
>
|
||||
<span class="text-sm text-muted-foreground ml-1"
|
||||
>used of {(totalStorage / 1024).toFixed(2)} GB</span
|
||||
>
|
||||
</div>
|
||||
<span class="font-medium text-sm text-muted-foreground"
|
||||
>{usagePercent.toFixed(1)}%</span
|
||||
>
|
||||
</div>
|
||||
<Progress value={usagePercent} class="h-2" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
You are using {usagePercent.toFixed(1)}% of available storage.
|
||||
</p>
|
||||
<Progress value={usagePercent} class="h-2.5 w-full" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4">Your Databases</h2>
|
||||
{#if userDatabases.length === 0}
|
||||
<div
|
||||
class="rounded-lg border border-dashed p-8 text-center text-muted-foreground mb-8"
|
||||
>
|
||||
No databases created yet.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-4 mb-8">
|
||||
{#each userDatabases as db}
|
||||
<div
|
||||
class="flex items-center justify-between rounded-lg border p-4 bg-muted/20"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="rounded-full bg-blue-500/10 p-2">
|
||||
<Database class="h-6 w-6 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold">{db.name}</h3>
|
||||
<p class="text-sm text-muted-foreground uppercase">
|
||||
{db.type} • {new Date(db.CreatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-medium px-2.5 py-0.5 rounded-full bg-green-500/15 text-green-500"
|
||||
>
|
||||
{db.status}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4">Create New</h2>
|
||||
<div class="grid gap-4">
|
||||
{#each availableTypes as db}
|
||||
<div
|
||||
class="flex items-center justify-between rounded-lg border p-4 hover:bg-muted/50 transition-colors"
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h2
|
||||
class="text-xl font-semibold tracking-tight mb-4 flex items-center gap-2"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="rounded-full bg-primary/10 p-2">
|
||||
<Server class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold">{db.name}</h3>
|
||||
<p class="text-sm text-muted-foreground">{db.description}</p>
|
||||
</div>
|
||||
<Server class="w-5 h-5 text-muted-foreground" />
|
||||
Active Databases
|
||||
</h2>
|
||||
|
||||
{#if loading}
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each Array(3) as _}
|
||||
<div
|
||||
class="h-32 rounded-lg bg-muted/20 animate-pulse"
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span
|
||||
class="text-xs font-medium px-2.5 py-0.5 rounded-full {db.status ===
|
||||
'Available'
|
||||
? 'bg-green-500/15 text-green-500'
|
||||
: 'bg-yellow-500/15 text-yellow-500'}"
|
||||
{:else if userDatabases.length === 0}
|
||||
<div
|
||||
class="rounded-xl border border-dashed p-10 text-center bg-muted/10"
|
||||
>
|
||||
<div
|
||||
class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-muted"
|
||||
>
|
||||
{db.status}
|
||||
</span>
|
||||
<Database class="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 class="mt-4 text-lg font-semibold">No databases yet</h3>
|
||||
<p class="mb-4 text-sm text-muted-foreground">
|
||||
Create your first database to get started.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each userDatabases as db}
|
||||
<button
|
||||
class="flex items-center justify-between p-4 rounded-xl border bg-card hover:shadow-md transition-all group text-left relative overflow-hidden"
|
||||
onclick={() => handleCardClick(db)}
|
||||
disabled={db.type !== "mongodb"}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class={`rounded-full p-2.5 ${
|
||||
db.type === "sqlite"
|
||||
? "bg-blue-500/10"
|
||||
: db.type === "mongodb"
|
||||
? "bg-green-500/10"
|
||||
: "bg-muted"
|
||||
}`}
|
||||
>
|
||||
{#if db.type === "sqlite"}
|
||||
<Box class="h-5 w-5 text-blue-500" />
|
||||
{:else if db.type === "mongodb"}
|
||||
<Database
|
||||
class="h-5 w-5 text-green-500"
|
||||
/>
|
||||
{:else}
|
||||
<Database
|
||||
class="h-5 w-5 text-muted-foreground"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-base">
|
||||
{db.name}
|
||||
</h3>
|
||||
<div
|
||||
class="flex items-center gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<span class="capitalize">{db.type}</span
|
||||
>
|
||||
<span>•</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div
|
||||
class={`h-2 w-2 rounded-full ${db.status === "running" || db.status === "Available" ? "bg-green-500" : "bg-yellow-500"}`}
|
||||
></div>
|
||||
<span class="capitalize"
|
||||
>{db.status}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
{#if db.port > 0}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="font-mono"
|
||||
>
|
||||
:{db.port}
|
||||
</Badge>
|
||||
{/if}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="text-muted-foreground hover:text-red-500 z-10"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(db.ID);
|
||||
}}
|
||||
>
|
||||
<Trash class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="pt-8">
|
||||
<h2
|
||||
class="text-xl font-semibold tracking-tight mb-4 flex items-center gap-2"
|
||||
>
|
||||
<Plus class="w-5 h-5 text-muted-foreground" />
|
||||
Create New Database
|
||||
</h2>
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{#each availableTypes as db}
|
||||
<button
|
||||
class="flex flex-col items-start text-left rounded-xl border p-4 hover:bg-muted/30 hover:border-primary/50 transition-all disabled:opacity-50 disabled:pointer-events-none group"
|
||||
disabled={db.status !== "Available"}
|
||||
onclick={() => initiateCreate(db.type)}
|
||||
>
|
||||
<div
|
||||
class="flex w-full items-center justify-between mb-3"
|
||||
>
|
||||
<div class={`rounded-full p-2.5 ${db.bgColor}`}>
|
||||
<db.icon class={`h-5 w-5 ${db.color}`} />
|
||||
</div>
|
||||
{#if db.status === "Available"}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="bg-primary/10 text-primary hover:bg-primary/20"
|
||||
>Available</Badge
|
||||
>
|
||||
{:else}
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-muted-foreground">Soon</Badge
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg">{db.name}</h3>
|
||||
<p
|
||||
class="text-sm text-muted-foreground mt-1 leading-snug"
|
||||
>
|
||||
{db.description}
|
||||
</p>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creation Dialog -->
|
||||
<Dialog.Root bind:open={showCreateDialog}>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title
|
||||
>Create {availableTypes.find((t) => t.type === selectedDbType)
|
||||
?.name} Database</Dialog.Title
|
||||
>
|
||||
<Dialog.Description>
|
||||
Enter a unique name for your new database instance.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="name" class="text-right">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
bind:value={newDbName}
|
||||
placeholder="my-database-1"
|
||||
class="col-span-3"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (showCreateDialog = false)}
|
||||
>Cancel</Button
|
||||
>
|
||||
<Button
|
||||
onclick={performCreate}
|
||||
disabled={isCreating || !newDbName.trim()}
|
||||
>
|
||||
{isCreating ? "Creating..." : "Create Database"}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<!-- Credentials Dialog -->
|
||||
<Dialog.Root bind:open={showCredsDialog}>
|
||||
<Dialog.Content class="sm:max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="flex items-center gap-2">
|
||||
<Check class="h-5 w-5 text-green-500" /> Database Created
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Your database is ready. These variables are only shown once, so
|
||||
please copy them now.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="flex flex-col space-y-4 py-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Connection URI</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
readonly
|
||||
value={newCreds.uri}
|
||||
class="font-mono bg-muted text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={db.status !== "Available"}
|
||||
onclick={() => handleCreate(db.type)}
|
||||
size="icon"
|
||||
onclick={() => copyToClipboard(newCreds.uri)}
|
||||
>
|
||||
Create
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Username</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
readonly
|
||||
value={newCreds.username}
|
||||
class="font-mono bg-muted text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={() => copyToClipboard(newCreds.username)}
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Password</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
readonly
|
||||
value={newCreds.password}
|
||||
type="password"
|
||||
class="font-mono bg-muted text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={() => copyToClipboard(newCreds.password)}
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-md bg-yellow-500/10 p-4 border border-yellow-500/20"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<AlertCircle class="h-5 w-5 text-yellow-500 mt-0.5" />
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium text-yellow-500">
|
||||
Important
|
||||
</p>
|
||||
<p
|
||||
class="text-sm text-yellow-600/90 dark:text-yellow-500/90"
|
||||
>
|
||||
This database is accessible via port <span
|
||||
class="font-mono font-bold"
|
||||
>{newCreds.port}</span
|
||||
>. Make sure to allow this port in your firewall if
|
||||
accessing externally.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button onclick={() => (showCredsDialog = false)}>Done</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<!-- Management Dialog -->
|
||||
<Dialog.Root bind:open={showManageDialog}>
|
||||
<Dialog.Content class="sm:max-w-[600px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="flex items-center gap-2 text-xl">
|
||||
<Database class="h-5 w-5 text-primary" /> Manage {manageDb?.name}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Configure connection details, credentials, and instance power
|
||||
state.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
{#if manageCreds.loading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div
|
||||
class="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
<Tabs.Root value="connection" class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-2">
|
||||
<Tabs.Trigger value="connection">Connection</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings"
|
||||
>Settings & Danger</Tabs.Trigger
|
||||
>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="connection" class="space-y-4 py-4">
|
||||
<div
|
||||
class="rounded-lg border bg-card text-card-foreground shadow-sm p-6 space-y-6"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium leading-none">Status</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Current state of the database container.
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant={manageDb?.status === "running"
|
||||
? "default"
|
||||
: "secondary"}
|
||||
class={`${manageDb?.status === "running" ? "bg-green-500 hover:bg-green-600" : ""}`}
|
||||
>
|
||||
{manageDb?.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Connection string</Label>
|
||||
<div class="relative">
|
||||
<Input
|
||||
readonly
|
||||
value={manageCreds.uri}
|
||||
class="pr-10 font-mono text-sm"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground"
|
||||
onclick={() =>
|
||||
copyToClipboard(manageCreds.uri)}
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Host</Label>
|
||||
<div
|
||||
class="p-2 rounded-md border bg-muted font-mono text-sm"
|
||||
>
|
||||
localhost
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Port</Label>
|
||||
<Input
|
||||
type="number"
|
||||
class="font-mono text-sm"
|
||||
bind:value={manageCreds.port}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
onclick={performUpdateCreds}
|
||||
disabled={isUpdatingCreds}
|
||||
>
|
||||
{isUpdatingCreds
|
||||
? "Saving Changes..."
|
||||
: "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="settings" class="space-y-4 py-4">
|
||||
<div class="rounded-lg border p-4 space-y-4">
|
||||
<h3 class="font-medium text-sm">Credentials</h3>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label>Username</Label>
|
||||
<Input
|
||||
bind:value={manageCreds.username}
|
||||
class="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Password</Label>
|
||||
<div class="relative">
|
||||
<Input
|
||||
bind:value={manageCreds.password}
|
||||
type="text"
|
||||
class="pr-10 font-mono"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="absolute right-0 top-0 h-full px-3"
|
||||
onclick={() =>
|
||||
copyToClipboard(
|
||||
manageCreds.password,
|
||||
)}
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onclick={performUpdateCreds}
|
||||
disabled={isUpdatingCreds}
|
||||
variant="default"
|
||||
>
|
||||
Update Credentials
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-lg border border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-950/10 p-4 space-y-4"
|
||||
>
|
||||
<div>
|
||||
<h3
|
||||
class="font-medium text-sm text-red-600 dark:text-red-400"
|
||||
>
|
||||
Power Actions
|
||||
</h3>
|
||||
<p
|
||||
class="text-sm text-red-600/70 dark:text-red-400/70"
|
||||
>
|
||||
Manage the running state of your database.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="border-red-200 hover:bg-red-100 hover:text-red-600 dark:border-red-900 dark:hover:bg-red-900/30"
|
||||
disabled={isPowerAction ||
|
||||
manageDb?.status === "stopped"}
|
||||
onclick={performStopDatabase}
|
||||
>
|
||||
<Power class="h-4 w-4 mr-2" />
|
||||
Stop
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="border-red-200 hover:bg-red-100 hover:text-red-600 dark:border-red-900 dark:hover:bg-red-900/30"
|
||||
disabled={isPowerAction}
|
||||
onclick={performRestartDatabase}
|
||||
>
|
||||
{#if manageDb?.status === "stopped"}
|
||||
<Play class="h-4 w-4 mr-2" /> Start
|
||||
{:else}
|
||||
<RotateCw class="h-4 w-4 mr-2" /> Restart
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
Reference in New Issue
Block a user