Admin Dashboard, Webdocs, LICENSE, webhook, and ID's update.
This commit is contained in:
@@ -15,7 +15,8 @@ A modern, responsive dashboard to manage your applications, monitor deployments,
|
||||
|
||||
## Features
|
||||
- **Project List**: View all deployed applications.
|
||||
- **Real-time Logs**: Watch builds and runtime logs live.
|
||||
- **Deployment History**: Track past builds and status.
|
||||
- **Real-time Logs**: Watch builds and runtime logs live via WebSockets.
|
||||
- **Redeploy**: Trigger manual redeployments.
|
||||
- **Environment Variables**: Manage runtime configuration.
|
||||
- **Responsive Design**: Works on desktop and mobile.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.10.1",
|
||||
"@internationalized/date": "^3.11.0",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
@@ -32,6 +32,8 @@
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^17.0.1",
|
||||
"svelte-sonner": "^1.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,23 @@ export interface EnvVar {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Deployment {
|
||||
id: string;
|
||||
project_id: string;
|
||||
status: string;
|
||||
commit: string;
|
||||
logs: string;
|
||||
url: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
ID: number;
|
||||
id: string;
|
||||
name: string;
|
||||
repo_url: string;
|
||||
port: number;
|
||||
deployments: any[];
|
||||
deployments: Deployment[];
|
||||
env_vars: EnvVar[];
|
||||
webhook_secret: string;
|
||||
git_token?: string;
|
||||
@@ -228,3 +239,33 @@ export async function createDatabase(name: string, type: string = "sqlite") {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminUsers() {
|
||||
try {
|
||||
return await fetchWithAuth("/api/admin/users");
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAdminUser(id: string) {
|
||||
try {
|
||||
await fetchWithAuth(`/api/admin/users/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminStats() {
|
||||
try {
|
||||
return await fetchWithAuth("/api/admin/stats");
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export interface User {
|
||||
ID: number;
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
is_admin: boolean;
|
||||
}
|
||||
|
||||
const storedUser = browser ? localStorage.getItem('user') : null;
|
||||
|
||||
@@ -42,15 +42,13 @@
|
||||
class="flex flex-wrap justify-center gap-4 text-sm text-muted-foreground"
|
||||
>
|
||||
<a href="/" class="hover:text-foreground transition-colors">Home</a>
|
||||
<a href="/" class="hover:text-foreground transition-colors">Docs</a>
|
||||
<a href="/docs" class="hover:text-foreground transition-colors">Docs</a>
|
||||
<a
|
||||
href="https://github.com/SirBlobby/Clickploy"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="hover:text-foreground transition-colors">GitHub</a
|
||||
>
|
||||
<a href="/" class="hover:text-foreground transition-colors">Support</a>
|
||||
<a href="/" class="hover:text-foreground transition-colors">Legal</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +61,11 @@
|
||||
<span class={isNormal ? "text-blue-500" : "text-red-500"}>
|
||||
{isNormal ? "All systems normal." : status}
|
||||
</span>
|
||||
{#if version}
|
||||
<span class="text-muted-foreground ml-2">
|
||||
{version}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
Rocket,
|
||||
Network,
|
||||
Database,
|
||||
Shield,
|
||||
Book,
|
||||
} from "@lucide/svelte";
|
||||
import { page } from "$app/stores";
|
||||
import * as Sheet from "$lib/components/ui/sheet";
|
||||
@@ -35,48 +37,83 @@
|
||||
|
||||
{#if $user}
|
||||
<nav class="hidden md:flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" href="/" class={isActive("/")}>
|
||||
<LayoutDashboard class="mr-2 h-4 w-4" /> Overview
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
href="/"
|
||||
class={`group ${isActive("/")}`}
|
||||
>
|
||||
<LayoutDashboard class="h-4 w-4" />
|
||||
<span
|
||||
class={`max-w-0 overflow-hidden group-hover:max-w-xs transition-all duration-300 ease-in-out whitespace-nowrap opacity-0 group-hover:opacity-100 group-hover:ml-2 ${$page.url.pathname === "/" ? "max-w-xs opacity-100 ml-2" : ""}`}
|
||||
>
|
||||
Overview
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
href="/deployments"
|
||||
class={isActive("/deployments")}
|
||||
class={`group ${isActive("/deployments")}`}
|
||||
>
|
||||
<Rocket class="mr-2 h-4 w-4" /> Deployments
|
||||
<Rocket class="h-4 w-4" />
|
||||
<span
|
||||
class={`max-w-0 overflow-hidden group-hover:max-w-xs transition-all duration-300 ease-in-out whitespace-nowrap opacity-0 group-hover:opacity-100 group-hover:ml-2 ${$page.url.pathname === "/deployments" ? "max-w-xs opacity-100 ml-2" : ""}`}
|
||||
>
|
||||
Deployments
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
href="/network"
|
||||
class={isActive("/network")}
|
||||
class={`group ${isActive("/network")}`}
|
||||
>
|
||||
<Network class="mr-2 h-4 w-4" /> Network
|
||||
<Network class="h-4 w-4" />
|
||||
<span
|
||||
class={`max-w-0 overflow-hidden group-hover:max-w-xs transition-all duration-300 ease-in-out whitespace-nowrap opacity-0 group-hover:opacity-100 group-hover:ml-2 ${$page.url.pathname === "/network" ? "max-w-xs opacity-100 ml-2" : ""}`}
|
||||
>
|
||||
Network
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
href="/activity"
|
||||
class={isActive("/activity")}
|
||||
class={`group ${isActive("/activity")}`}
|
||||
>
|
||||
<Activity class="mr-2 h-4 w-4" /> Activity
|
||||
<Activity class="h-4 w-4" />
|
||||
<span
|
||||
class={`max-w-0 overflow-hidden group-hover:max-w-xs transition-all duration-300 ease-in-out whitespace-nowrap opacity-0 group-hover:opacity-100 group-hover:ml-2 ${$page.url.pathname === "/activity" ? "max-w-xs opacity-100 ml-2" : ""}`}
|
||||
>
|
||||
Activity
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
href="/storage"
|
||||
class={isActive("/storage")}
|
||||
class={`group ${isActive("/storage")}`}
|
||||
>
|
||||
<Database class="mr-2 h-4 w-4" /> Storage
|
||||
<Database class="h-4 w-4" />
|
||||
<span
|
||||
class={`max-w-0 overflow-hidden group-hover:max-w-xs transition-all duration-300 ease-in-out whitespace-nowrap opacity-0 group-hover:opacity-100 group-hover:ml-2 ${$page.url.pathname === "/storage" ? "max-w-xs opacity-100 ml-2" : ""}`}
|
||||
>
|
||||
Storage
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
href="/settings"
|
||||
class={isActive("/settings")}
|
||||
href="/docs"
|
||||
class={`group ${isActive("/docs")}`}
|
||||
>
|
||||
<Settings class="mr-2 h-4 w-4" /> Settings
|
||||
<Book class="h-4 w-4" />
|
||||
<span
|
||||
class={`max-w-0 overflow-hidden group-hover:max-w-xs transition-all duration-300 ease-in-out whitespace-nowrap opacity-0 group-hover:opacity-100 group-hover:ml-2 ${$page.url.pathname.startsWith("/docs") ? "max-w-xs opacity-100 ml-2" : ""}`}
|
||||
>
|
||||
Docs
|
||||
</span>
|
||||
</Button>
|
||||
</nav>
|
||||
{/if}
|
||||
@@ -84,6 +121,26 @@
|
||||
|
||||
{#if $user}
|
||||
<div class="flex items-center gap-4">
|
||||
<nav class="hidden md:flex items-center gap-2 mr-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
href="/settings"
|
||||
class={isActive("/settings")}
|
||||
>
|
||||
<Settings class="h-4 w-4" />
|
||||
</Button>
|
||||
{#if $user.is_admin}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
href="/admin"
|
||||
class={isActive("/admin")}
|
||||
>
|
||||
<Shield class="h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</nav>
|
||||
<div class="hidden md:flex items-center gap-2">
|
||||
<div
|
||||
class="h-8 w-8 rounded-full bg-linear-to-tr from-primary to-purple-500"
|
||||
@@ -147,6 +204,13 @@
|
||||
>
|
||||
<Database class="h-5 w-5" /> Storage
|
||||
</a>
|
||||
<a
|
||||
href="/docs"
|
||||
class="flex items-center gap-2 py-2 text-lg font-medium"
|
||||
onclick={() => (mobileOpen = false)}
|
||||
>
|
||||
<Book class="h-5 w-5" /> Docs
|
||||
</a>
|
||||
<a
|
||||
href="/settings"
|
||||
class="flex items-center gap-2 py-2 text-lg font-medium"
|
||||
@@ -154,6 +218,15 @@
|
||||
>
|
||||
<Settings class="h-5 w-5" /> Settings
|
||||
</a>
|
||||
{#if $user.is_admin}
|
||||
<a
|
||||
href="/admin"
|
||||
class="flex items-center gap-2 py-2 text-lg font-medium"
|
||||
onclick={() => (mobileOpen = false)}
|
||||
>
|
||||
<Shield class="h-5 w-5" /> Admin
|
||||
</a>
|
||||
{/if}
|
||||
<div class="border-t my-2"></div>
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<div
|
||||
@@ -175,6 +248,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex gap-2">
|
||||
<Button variant="ghost" size="sm" href="/docs">Docs</Button>
|
||||
<Button variant="ghost" size="sm" href="/login">Login</Button>
|
||||
<Button size="sm" href="/register">Get Started</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ export {
|
||||
Root,
|
||||
Description,
|
||||
Title,
|
||||
//
|
||||
Root as Alert,
|
||||
Description as AlertDescription,
|
||||
Title as AlertTitle,
|
||||
|
||||
@@ -8,7 +8,6 @@ import Root, {
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
|
||||
@@ -14,7 +14,6 @@ export {
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
|
||||
@@ -6,7 +6,6 @@ export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
//
|
||||
Root as Collapsible,
|
||||
Content as CollapsibleContent,
|
||||
Trigger as CollapsibleTrigger,
|
||||
|
||||
@@ -20,7 +20,6 @@ export {
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
|
||||
@@ -2,6 +2,5 @@ import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,5 @@ import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,5 @@ import Root from "./progress.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Progress,
|
||||
};
|
||||
|
||||
@@ -22,7 +22,6 @@ export {
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
Portal,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
|
||||
@@ -2,6 +2,5 @@ import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@ export {
|
||||
Footer,
|
||||
Title,
|
||||
Description,
|
||||
//
|
||||
Root as Sheet,
|
||||
Close as SheetClose,
|
||||
Trigger as SheetTrigger,
|
||||
|
||||
@@ -16,7 +16,6 @@ export {
|
||||
Head,
|
||||
Header,
|
||||
Row,
|
||||
//
|
||||
Root as Table,
|
||||
Body as TableBody,
|
||||
Caption as TableCaption,
|
||||
|
||||
30
frontend/src/routes/+error.svelte
Normal file
30
frontend/src/routes/+error.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { AlertTriangle, Home, ArrowLeft } from "@lucide/svelte";
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center min-h-[70vh] text-center px-4 animate-in fade-in zoom-in duration-300">
|
||||
<div class="bg-destructive/10 p-6 rounded-full mb-6">
|
||||
<AlertTriangle class="h-12 w-12 text-destructive" />
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl font-bold tracking-tight mb-2">
|
||||
{$page.status}
|
||||
</h1>
|
||||
|
||||
<p class="text-xl text-muted-foreground mb-8 max-w-md text-balance">
|
||||
{$page.error?.message || "Something went wrong."}
|
||||
</p>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<Button variant="outline" onclick={() => history.back()}>
|
||||
<ArrowLeft class="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
</Button>
|
||||
<Button href="/">
|
||||
<Home class="mr-2 h-4 w-4" />
|
||||
Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
1
frontend/src/routes/+layout.ts
Normal file
1
frontend/src/routes/+layout.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ssr = false;
|
||||
@@ -22,6 +22,7 @@
|
||||
Settings,
|
||||
ChevronsUpDown,
|
||||
ExternalLink,
|
||||
Upload,
|
||||
} from "@lucide/svelte";
|
||||
import * as Collapsible from "$lib/components/ui/collapsible";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
@@ -59,6 +60,48 @@
|
||||
envVars = envVars.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
async function handleEnvUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (!input.files || input.files.length === 0) return;
|
||||
|
||||
const file = input.files[0];
|
||||
const text = await file.text();
|
||||
|
||||
const newVars: { key: string; value: string }[] = [];
|
||||
const lines = text.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
|
||||
const eqIdx = trimmed.indexOf('=');
|
||||
if (eqIdx === -1) continue;
|
||||
|
||||
const key = trimmed.slice(0, eqIdx).trim();
|
||||
let value = trimmed.slice(eqIdx + 1).trim();
|
||||
|
||||
// Remove surrounding quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
if (key) {
|
||||
newVars.push({ key, value });
|
||||
}
|
||||
}
|
||||
|
||||
if (newVars.length > 0) {
|
||||
// If the first item is empty, remove it
|
||||
if (envVars.length === 1 && !envVars[0].key && !envVars[0].value) {
|
||||
envVars = newVars;
|
||||
} else {
|
||||
envVars = [...envVars, ...newVars];
|
||||
}
|
||||
}
|
||||
|
||||
input.value = ''; // Reset input
|
||||
}
|
||||
|
||||
async function handleDeploy() {
|
||||
if (!repo || !name) return;
|
||||
deploying = true;
|
||||
@@ -108,21 +151,99 @@
|
||||
<div class="container mx-auto py-10 px-4">
|
||||
{#if !$user}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center space-y-10 py-20 text-center"
|
||||
class="flex flex-col items-center justify-center min-h-[60vh] text-center space-y-8 animate-in fade-in zoom-in duration-500"
|
||||
>
|
||||
Deploy with <span
|
||||
class="bg-linear-to-r from-blue-400 to-purple-600 bg-clip-text text-transparent"
|
||||
>One Click</span
|
||||
>.
|
||||
<p class="max-w-[600px] text-muted-foreground text-xl">
|
||||
The simplified PaaS for your personal projects. Push, build, and scale
|
||||
without the complexity.
|
||||
</p>
|
||||
<div class="flex gap-4">
|
||||
<Button href="/login" size="lg">Get Started</Button>
|
||||
<Button href="https://github.com/clickploy" variant="outline" size="lg">
|
||||
<Github class="mr-2 h-4 w-4" /> GitHub
|
||||
<div
|
||||
class="bg-primary/10 p-4 rounded-full mb-4 ring-1 ring-primary/20 shadow-[0_0_30px_-10px_rgba(255,255,255,0.3)]"
|
||||
>
|
||||
<Terminal class="w-12 h-12 text-primary" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 max-w-2xl">
|
||||
<h1 class="text-4xl md:text-6xl font-extrabold tracking-tight">
|
||||
Deploy with Clickploy
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground leading-relaxed">
|
||||
Self-hosted PaaS made simple. Push your code, we handle the rest. No
|
||||
complex configs, just pure deployment power.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 w-full sm:w-auto pt-4">
|
||||
<Button
|
||||
href="/login"
|
||||
size="lg"
|
||||
class="min-w-[160px] text-lg h-12 shadow-lg hover:shadow-primary/25 transition-all"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
<Button
|
||||
href="https://github.com/SirBlobby/Clickploy"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="min-w-[160px] text-lg h-12"
|
||||
>
|
||||
<Github class="mr-2 h-5 w-5" /> GitHub
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pt-12 w-full max-w-5xl text-left"
|
||||
>
|
||||
<Card class="bg-card/50 border-muted">
|
||||
<CardHeader class="pb-2">
|
||||
<Activity class="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle class="text-lg">Zero Config</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="text-sm text-muted-foreground">
|
||||
Deploy from any Git repo without writing Dockerfiles or YAML.
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-card/50 border-muted">
|
||||
<CardHeader class="pb-2">
|
||||
<Terminal class="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle class="text-lg">Docker Native</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="text-sm text-muted-foreground">
|
||||
Every app runs in its own isolated container for security.
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-card/50 border-muted">
|
||||
<CardHeader class="pb-2">
|
||||
<ExternalLink class="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle class="text-lg">Auto Ports</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="text-sm text-muted-foreground">
|
||||
We automatically assign and manage ports for your services.
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-card/50 border-muted">
|
||||
<CardHeader class="pb-2">
|
||||
<Settings class="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle class="text-lg">Full Control</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="text-sm text-muted-foreground">
|
||||
Self-hosted means you own your data, logs, and infrastructure.
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-card/50 border-muted">
|
||||
<CardHeader class="pb-2">
|
||||
<Terminal class="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle class="text-lg">CLI Integration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="text-sm text-muted-foreground">
|
||||
Manage your deployments from the terminal. (Coming Soon)
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-card/50 border-muted">
|
||||
<CardHeader class="pb-2">
|
||||
<Github class="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle class="text-lg">Git Webhooks</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="text-sm text-muted-foreground">
|
||||
Automatically deploy when you push changes to your repository.
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -165,14 +286,14 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
readonly
|
||||
value={`http://localhost:8080/webhooks/trigger?project_id=${createdProject.ID}`}
|
||||
value={`http://localhost:8080/webhooks/trigger?project_id=${createdProject.id}`}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
`http://localhost:8080/webhooks/trigger?project_id=${createdProject.ID}`,
|
||||
`http://localhost:8080/webhooks/trigger?project_id=${createdProject.id}`,
|
||||
)}
|
||||
>
|
||||
Copy
|
||||
@@ -341,6 +462,24 @@
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" /> Add Variable
|
||||
</Button>
|
||||
|
||||
<div class="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept=".env,text/plain"
|
||||
class="hidden"
|
||||
id="env-upload"
|
||||
onchange={handleEnvUpload}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
onclick={() => document.getElementById('env-upload')?.click()}
|
||||
>
|
||||
<Upload class="mr-2 h-4 w-4" /> Upload .env File
|
||||
</Button>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
@@ -391,16 +530,16 @@
|
||||
<Card
|
||||
class="group hover:shadow-lg transition-all duration-300 border-muted/60 hover:border-primary/50 cursor-pointer overflow-hidden relative"
|
||||
>
|
||||
<a href={`/projects/${project.ID}`} class="block h-full">
|
||||
<a href={`/projects/${project.id}`} class="block h-full">
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="space-y-1">
|
||||
<CardTitle class="text-xl flex items-center gap-2">
|
||||
{project.name}
|
||||
</CardTitle>
|
||||
<CardDescription class="flex items-center gap-1">
|
||||
<CardDescription class="flex items-center gap-1" >
|
||||
<Github class="h-3 w-3" />
|
||||
{new URL(project.repo_url).pathname.slice(1)}
|
||||
<a href={project.repo_url} target="_blank">{new URL(project.repo_url).pathname.slice(1)}</a>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<span
|
||||
@@ -464,7 +603,7 @@
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors"
|
||||
href={`/projects/${project.ID}`}
|
||||
href={`/projects/${project.id}`}
|
||||
>
|
||||
Manage
|
||||
</Button>
|
||||
|
||||
@@ -60,10 +60,10 @@
|
||||
<div
|
||||
class="grid grid-cols-12 gap-4 px-4 py-3 border-b text-xs font-medium text-muted-foreground bg-muted/40 uppercase tracking-wider"
|
||||
>
|
||||
<div class="col-span-3">Project</div>
|
||||
<div class="col-span-4">Commit</div>
|
||||
<div class="col-span-2">Status</div>
|
||||
<div class="col-span-3 text-right">Time</div>
|
||||
<div class="col-span-6 md:col-span-3">Project</div>
|
||||
<div class="hidden md:block col-span-4">Commit</div>
|
||||
<div class="col-span-4 md:col-span-2">Status</div>
|
||||
<div class="col-span-2 md:col-span-3 text-right">Time</div>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-border/40">
|
||||
@@ -71,7 +71,7 @@
|
||||
<div
|
||||
class="grid grid-cols-12 gap-4 px-4 py-3 items-center hover:bg-muted/30 transition-colors text-sm group"
|
||||
>
|
||||
<div class="col-span-3 flex items-center gap-2 overflow-hidden">
|
||||
<div class="col-span-6 md:col-span-3 flex items-center gap-2 overflow-hidden">
|
||||
<div
|
||||
class="h-8 w-8 rounded bg-primary/10 flex items-center justify-center shrink-0 text-primary uppercase font-bold text-xs ring-1 ring-inset ring-primary/20"
|
||||
>
|
||||
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-span-4 flex items-center gap-2 font-mono text-xs text-muted-foreground"
|
||||
class="hidden md:flex col-span-4 items-center gap-2 font-mono text-xs text-muted-foreground"
|
||||
>
|
||||
<GitCommit class="h-3.5 w-3.5 shrink-0" />
|
||||
<span
|
||||
@@ -103,7 +103,7 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="col-span-4 md:col-span-2">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium border
|
||||
{activity.status === 'live'
|
||||
@@ -122,9 +122,9 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-span-3 flex items-center justify-end gap-3 text-right"
|
||||
class="col-span-2 md:col-span-3 flex items-center justify-end gap-3 text-right"
|
||||
>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<span class="hidden md:inline text-xs text-muted-foreground">
|
||||
{new Date(activity.CreatedAt).toLocaleString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
|
||||
158
frontend/src/routes/admin/+page.svelte
Normal file
158
frontend/src/routes/admin/+page.svelte
Normal file
@@ -0,0 +1,158 @@
|
||||
<script lang="ts">
|
||||
import { getAdminUsers, deleteAdminUser, getAdminStats } from "$lib/api";
|
||||
import { user, type User } from "$lib/auth";
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "$lib/components/ui/table";
|
||||
import { Loader2, Trash2, Shield, Users, Box, Layers } from "@lucide/svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
let loading = true;
|
||||
let users: any[] = [];
|
||||
let stats: any = null;
|
||||
|
||||
onMount(async () => {
|
||||
if (!$user || !$user.is_admin) {
|
||||
toast.error("Unauthorized");
|
||||
goto("/");
|
||||
return;
|
||||
}
|
||||
|
||||
await loadData();
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
loading = true;
|
||||
const [u, s] = await Promise.all([getAdminUsers(), getAdminStats()]);
|
||||
users = u || [];
|
||||
stats = s;
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function handleDeleteUser(id: string) {
|
||||
if (!confirm("Are you sure you want to delete this user?")) return;
|
||||
const success = await deleteAdminUser(id);
|
||||
if (success) {
|
||||
toast.success("User deleted");
|
||||
users = users.filter((u) => u.id !== id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto py-10 px-4">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold tracking-tight">Admin Dashboard</h2>
|
||||
<p class="text-muted-foreground">System overview and user management.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center p-20">
|
||||
<Loader2 class="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Stats Cards -->
|
||||
{#if stats}
|
||||
<div class="grid gap-4 md:grid-cols-3 mb-8">
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle class="text-sm font-medium">Total Users</CardTitle>
|
||||
<Users class="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{stats.users}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle class="text-sm font-medium">Total Projects</CardTitle>
|
||||
<Box class="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{stats.projects}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle class="text-sm font-medium">Total Deployments</CardTitle>
|
||||
<Layers class="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{stats.deployments}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Users Table -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Users</CardTitle>
|
||||
<CardDescription>Manage registered users.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Projects</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead class="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each users as u}
|
||||
<TableRow>
|
||||
<TableCell>{u.id}</TableCell>
|
||||
<TableCell class="font-medium">{u.name}</TableCell>
|
||||
<TableCell>{u.email}</TableCell>
|
||||
<TableCell>{u.projects?.length || 0}</TableCell>
|
||||
<TableCell>
|
||||
{#if u.is_admin}
|
||||
<span
|
||||
class="inline-flex items-center rounded-full border border-transparent bg-primary/10 px-2.5 py-0.5 text-xs font-semibold text-primary"
|
||||
>
|
||||
<Shield class="mr-1 h-3 w-3" /> Admin
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-muted-foreground">User</span>
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
{#if !u.is_admin}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="text-destructive hover:text-destructive/90"
|
||||
onclick={() => handleDeleteUser(u.id)}
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -67,9 +67,9 @@
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Project</TableHead>
|
||||
<TableHead>Commit</TableHead>
|
||||
<TableHead class="hidden md:table-cell">Commit</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead class="hidden md:table-cell">Created</TableHead>
|
||||
<TableHead class="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -79,7 +79,7 @@
|
||||
<TableCell class="font-medium">
|
||||
{#if deploy.project}
|
||||
<a
|
||||
href={`/projects/${deploy.project.ID}`}
|
||||
href={`/projects/${deploy.project.id}`}
|
||||
class="hover:underline flex items-center gap-2"
|
||||
>
|
||||
{deploy.project.name}
|
||||
@@ -88,7 +88,7 @@
|
||||
<span class="text-muted-foreground">Deleted Project</span>
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell class="hidden md:table-cell">
|
||||
<div class="flex items-center gap-2">
|
||||
<GitCommit class="h-4 w-4 text-muted-foreground" />
|
||||
<span class="font-mono text-sm"
|
||||
@@ -108,7 +108,7 @@
|
||||
{deploy.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-muted-foreground">
|
||||
<TableCell class="hidden md:table-cell text-muted-foreground">
|
||||
{new Date(deploy.CreatedAt).toLocaleDateString()}
|
||||
{new Date(deploy.CreatedAt).toLocaleTimeString()}
|
||||
</TableCell>
|
||||
|
||||
48
frontend/src/routes/docs/+layout.svelte
Normal file
48
frontend/src/routes/docs/+layout.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Book } from "@lucide/svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
function isActive(path: string) {
|
||||
return $page.url.pathname === path ? "bg-secondary" : "hover:bg-accent/50";
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ href: "/docs", label: "Introduction" },
|
||||
{ href: "/docs/features", label: "Features" },
|
||||
{ href: "/docs/architecture", label: "Architecture" },
|
||||
{ href: "/docs/api", label: "API Reference" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto py-10 px-4">
|
||||
<div class="flex flex-col md:flex-row gap-10">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="hidden md:block w-64 shrink-0 space-y-8">
|
||||
<div class="sticky top-20">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<Book class="h-6 w-6 text-primary" />
|
||||
<h2 class="text-xl font-bold tracking-tight">Documentation</h2>
|
||||
</div>
|
||||
<nav class="space-y-1">
|
||||
{#each navItems as item}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full justify-start {isActive(item.href)}"
|
||||
href={item.href}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 space-y-12 max-w-4xl min-h-[50vh]">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
23
frontend/src/routes/docs/+page.svelte
Normal file
23
frontend/src/routes/docs/+page.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
Introduction
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground">
|
||||
A minimal, self-hosted Platform as a Service (PaaS) for building and
|
||||
deploying applications quickly.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none">
|
||||
<p>
|
||||
Clickploy is designed to be a simple, powerful alternative to complex
|
||||
container orchestration platforms. It leverages Docker and Nixpacks to turn
|
||||
your Git repositories into running applications with zero configuration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
132
frontend/src/routes/docs/api/+page.svelte
Normal file
132
frontend/src/routes/docs/api/+page.svelte
Normal file
@@ -0,0 +1,132 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "$lib/components/ui/card";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "$lib/components/ui/table";
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
API Reference
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground">
|
||||
The backend exposes a RESTful API for automation and integration. All
|
||||
authenticated endpoints require a <code>Bearer</code> token.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Project Endpoints -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-xl font-semibold">Projects</h3>
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[100px]">Method</TableHead>
|
||||
<TableHead>Endpoint</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell><Badge>GET</Badge></TableCell>
|
||||
<TableCell class="font-mono">/api/projects</TableCell>
|
||||
<TableCell>List all projects for the current user.</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell><Badge>POST</Badge></TableCell>
|
||||
<TableCell class="font-mono">/api/projects</TableCell>
|
||||
<TableCell>Create and deploy a new project.</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell><Badge variant="secondary">GET</Badge></TableCell>
|
||||
<TableCell class="font-mono">/api/projects/:id</TableCell>
|
||||
<TableCell>Get details of a specific project.</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell><Badge>POST</Badge></TableCell>
|
||||
<TableCell class="font-mono">/api/projects/:id/redeploy</TableCell>
|
||||
<TableCell>Trigger a manual redeployment.</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell><Badge variant="outline">PUT</Badge></TableCell>
|
||||
<TableCell class="font-mono">/api/projects/:id/env</TableCell>
|
||||
<TableCell>Update environment variables.</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Activity & Stats -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-xl font-semibold">Activity & Data</h3>
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[100px]">Method</TableHead>
|
||||
<TableHead>Endpoint</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell><Badge variant="secondary">GET</Badge></TableCell>
|
||||
<TableCell class="font-mono">/api/activity</TableCell>
|
||||
<TableCell>Get recent deployment activity.</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- System Endpoints -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-xl font-semibold">System & Admin</h3>
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[100px]">Method</TableHead>
|
||||
<TableHead>Endpoint</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell><Badge variant="secondary">GET</Badge></TableCell>
|
||||
<TableCell class="font-mono">/api/system/status</TableCell>
|
||||
<TableCell>Check backend health and version.</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell><Badge variant="secondary">GET</Badge></TableCell>
|
||||
<TableCell class="font-mono">/api/admin/stats</TableCell>
|
||||
<TableCell>Get global system statistics (Admin only).</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell><Badge variant="secondary">GET</Badge></TableCell>
|
||||
<TableCell class="font-mono">/api/admin/users</TableCell>
|
||||
<TableCell>List all users (Admin only).</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
44
frontend/src/routes/docs/architecture/+page.svelte
Normal file
44
frontend/src/routes/docs/architecture/+page.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
Architecture
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground">
|
||||
How Clickploy components interact.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<Card>
|
||||
<CardContent class="pt-6 space-y-4">
|
||||
<p>
|
||||
Clickploy operates as a control plane for your deployments. Here's how the
|
||||
components interact:
|
||||
</p>
|
||||
<ul class="list-disc pl-6 space-y-2 text-muted-foreground">
|
||||
<li>
|
||||
<strong class="text-foreground">Backend (Go):</strong> Handles API
|
||||
requests, manages the SQLite database, and orchestrates Docker
|
||||
containers.
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-foreground">Frontend (SvelteKit):</strong> Provides a
|
||||
reactive web interface for managing projects and viewing logs.
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-foreground">Builder (Nixpacks):</strong> Analyzes
|
||||
source code and generates OCI-compliant images.
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-foreground">Reverse Proxy:</strong> (Optional)
|
||||
Typically runs behind Nginx or Caddy for SSL termination.
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
158
frontend/src/routes/docs/features/+page.svelte
Normal file
158
frontend/src/routes/docs/features/+page.svelte
Normal file
@@ -0,0 +1,158 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "$lib/components/ui/card";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import {
|
||||
Globe,
|
||||
Server,
|
||||
Terminal,
|
||||
Database,
|
||||
Settings,
|
||||
Shield,
|
||||
HardDrive,
|
||||
} from "@lucide/svelte";
|
||||
</script>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
Features
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground">
|
||||
Explore the core capabilities of Clickploy and learn how to use them.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<!-- Deployment & Build -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Deployment & Build</h2>
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Globe class="h-5 w-5 text-primary" />
|
||||
Auto-Build System
|
||||
</CardTitle>
|
||||
<CardDescription>Zero-config builds using Nixpacks</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<p>
|
||||
Clickploy automatically detects your application's language and framework (Node.js, Python, Go, Rust, etc.) and builds a container image without needing a Dockerfile.
|
||||
</p>
|
||||
<div class="bg-muted p-3 rounded-md text-sm">
|
||||
<strong>How to use:</strong> Simply paste your Git repository URL when creating a new project. Clickploy handles the rest.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Server class="h-5 w-5 text-primary" />
|
||||
Isolated Containers
|
||||
</CardTitle>
|
||||
<CardDescription>Docker-based runtime</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<p>
|
||||
Every application runs in its own isolated Docker container. This ensures consistent performance, security, and prevents dependency conflicts between projects.
|
||||
</p>
|
||||
<div class="bg-muted p-3 rounded-md text-sm">
|
||||
<strong>How it works:</strong> The system assigns a unique port and manages the container lifecycle (start, stop, restart) automatically.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Management & Ops -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Management & Operations</h2>
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Terminal class="h-5 w-5 text-primary" />
|
||||
Real-time Observability
|
||||
</CardTitle>
|
||||
<CardDescription>Live logs and deployment history</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<p>
|
||||
Watch your build process and application logs stream in real-time via WebSockets. Access historical logs for any past deployment to debug issues.
|
||||
</p>
|
||||
<div class="bg-muted p-3 rounded-md text-sm">
|
||||
<strong>How to use:</strong> Open a project dashboard. The terminal window shows live logs. Click "History" to view past deployments.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Settings class="h-5 w-5 text-primary" />
|
||||
Environment Configuration
|
||||
</CardTitle>
|
||||
<CardDescription>Secrets and Variables</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<p>
|
||||
Manage environment variables (API keys, database URLs) securely. Variables are injected into the container at runtime.
|
||||
</p>
|
||||
<div class="bg-muted p-3 rounded-md text-sm">
|
||||
<strong>How to use:</strong> Go to the <strong>Settings</strong> tab in your project dashboard to add or update variables.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Storage & Admin -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Storage & Administration</h2>
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<HardDrive class="h-5 w-5 text-primary" />
|
||||
Managed Storage
|
||||
</CardTitle>
|
||||
<CardDescription>Integrated Databases</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<p>
|
||||
Provision lightweight SQLite databases instantly for your applications. Perfect for prototypes and small-to-medium apps.
|
||||
</p>
|
||||
<div class="bg-muted p-3 rounded-md text-sm">
|
||||
<strong>How to use:</strong> Navigate to the <strong>Storage</strong> page to create and manage databases.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Shield class="h-5 w-5 text-primary" />
|
||||
Admin Control
|
||||
</CardTitle>
|
||||
<CardDescription>User Management & Stats</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<p>
|
||||
Administrators have full visibility into system performance, user registration, and global deployment statistics.
|
||||
</p>
|
||||
<div class="bg-muted p-3 rounded-md text-sm">
|
||||
<strong>How to use:</strong> Admin users can access the <strong>Admin</strong> dashboard from the sidebar to manage users and view system health.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -88,7 +88,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<Server class="h-4 w-4 text-muted-foreground" />
|
||||
<a
|
||||
href={`/projects/${project.ID}`}
|
||||
href={`/projects/${project.id}`}
|
||||
class="hover:underline"
|
||||
>
|
||||
{project.name}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
let status = $derived(latestDeployment?.status || "unknown");
|
||||
|
||||
let activeDeploymentLogs = $state("");
|
||||
let activeDeploymentId = $state<number | null>(null);
|
||||
let activeDeploymentId = $state<string | null>(null);
|
||||
let ws = $state<WebSocket | null>(null);
|
||||
let logContentRef = $state<HTMLDivElement | null>(null);
|
||||
let copied = $state(false);
|
||||
@@ -63,7 +63,7 @@
|
||||
async function handleRedeploy() {
|
||||
if (!project) return;
|
||||
toast.info("Starting redeployment...");
|
||||
const success = await redeployProject(project.ID.toString());
|
||||
const success = await redeployProject(project.id.toString());
|
||||
if (success) {
|
||||
toast.success("Redeployment started!");
|
||||
setTimeout(loadProject, 1000);
|
||||
@@ -71,16 +71,16 @@
|
||||
}
|
||||
|
||||
function selectDeployment(deployment: any) {
|
||||
if (activeDeploymentId === deployment.ID) return;
|
||||
if (activeDeploymentId === deployment.id) return;
|
||||
|
||||
activeDeploymentId = deployment.ID;
|
||||
activeDeploymentId = deployment.id;
|
||||
activeDeploymentLogs = deployment.logs || "";
|
||||
userScrolled = false;
|
||||
autoScroll = true;
|
||||
scrollToBottom(true);
|
||||
|
||||
if (deployment.status === "building") {
|
||||
connectWebSocket(deployment.ID);
|
||||
connectWebSocket(deployment.id);
|
||||
} else {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
@@ -89,7 +89,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function connectWebSocket(deploymentId: number) {
|
||||
function connectWebSocket(deploymentId: string) {
|
||||
if (ws) ws.close();
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
ws = new WebSocket(
|
||||
@@ -143,7 +143,7 @@
|
||||
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
{:else if project}
|
||||
<div class="space-y-4 h-[calc(100vh-140px)] flex flex-col overflow-hidden">
|
||||
<div class="space-y-4 lg:h-[calc(100vh-140px)] flex flex-col lg:overflow-hidden">
|
||||
<div class="shrink-0 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -193,7 +193,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-4 gap-px bg-border/40 rounded-lg overflow-hidden border"
|
||||
class="grid grid-cols-2 lg:grid-cols-4 gap-px bg-border/40 rounded-lg overflow-hidden border"
|
||||
>
|
||||
<div
|
||||
class="bg-card p-3 flex flex-col justify-center items-center relative overflow-hidden group"
|
||||
@@ -260,7 +260,7 @@
|
||||
|
||||
<div class="flex-1 grid grid-cols-1 lg:grid-cols-4 gap-4 min-h-0">
|
||||
<Card
|
||||
class="flex flex-col min-h-0 bg-transparent shadow-none border-0 lg:col-span-1"
|
||||
class="flex flex-col min-h-0 h-[300px] lg:h-auto bg-transparent shadow-none border-0 lg:col-span-1"
|
||||
>
|
||||
<div class="flex items-center justify-between px-1 pb-2">
|
||||
<h3 class="text-sm font-semibold text-muted-foreground">History</h3>
|
||||
@@ -270,25 +270,29 @@
|
||||
{#each project.deployments as deployment}
|
||||
<button
|
||||
class="w-full flex items-center justify-between p-2.5 rounded-md border text-left transition-all text-xs
|
||||
{activeDeploymentId === deployment.ID
|
||||
{activeDeploymentId === deployment.id
|
||||
? 'bg-primary/5 border-primary/20 shadow-sm'
|
||||
: 'bg-card hover:bg-muted/50 border-input'}"
|
||||
onclick={() => selectDeployment(deployment)}
|
||||
>
|
||||
<div class="flex flex-col gap-0.5 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">#{deployment.ID}</span>
|
||||
<span class="font-semibold text-xs">#{deployment.id}</span>
|
||||
<span
|
||||
class="font-mono text-[10px] text-muted-foreground bg-muted px-1 rounded flex items-center gap-1"
|
||||
>
|
||||
<GitCommit class="h-2 w-2" />
|
||||
{deployment.commit
|
||||
? deployment.commit.substring(0, 7)
|
||||
: "HEAD"}
|
||||
{deployment.commit === "HEAD"
|
||||
? "HEAD"
|
||||
: deployment.commit === "MANUAL"
|
||||
? "Manual"
|
||||
: deployment.commit === "WEBHOOK"
|
||||
? "Webhook"
|
||||
: deployment.commit.substring(0, 7)}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-[10px] text-muted-foreground truncate">
|
||||
{new Date(deployment.CreatedAt).toLocaleString(undefined, {
|
||||
{new Date(deployment.created_at).toLocaleString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
@@ -322,14 +326,15 @@
|
||||
</Card>
|
||||
|
||||
<div
|
||||
class="lg:col-span-3 flex flex-col min-h-0 rounded-lg border bg-zinc-950 shadow-sm overflow-hidden border-border/40"
|
||||
class="lg:col-span-3 flex flex-col min-h-0 h-[500px] lg:h-auto rounded-lg border bg-card shadow-sm overflow-hidden border-border/40"
|
||||
>
|
||||
<div
|
||||
class="flex shrink-0 items-center justify-between px-3 py-2 bg-zinc-900/50 border-b border-white/5"
|
||||
class="flex shrink-0 items-center justify-between px-3 py-2 bg-muted/50 border-b border-border"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Terminal class="h-3.5 w-3.5 text-zinc-400" />
|
||||
<span class="text-xs font-mono text-zinc-300">
|
||||
<Terminal class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
|
||||
<span class="text-xs font-mono text-muted-foreground">
|
||||
{#if activeDeploymentId}
|
||||
build-log-{activeDeploymentId}.log
|
||||
{:else}
|
||||
@@ -365,7 +370,7 @@
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6 text-zinc-400 hover:text-white"
|
||||
class="h-6 w-6 text-muted-foreground hover:text-foreground"
|
||||
onclick={copyLogs}
|
||||
title="Copy Logs"
|
||||
>
|
||||
@@ -381,14 +386,14 @@
|
||||
<div
|
||||
bind:this={logContentRef}
|
||||
onscroll={handleScroll}
|
||||
class="flex-1 overflow-auto p-3 font-mono text-[11px] leading-relaxed text-gray-200 scrollbar-thin scrollbar-thumb-zinc-800 scrollbar-track-transparent selection:bg-white/20"
|
||||
class="flex-1 overflow-auto p-4 font-mono text-[11px] leading-relaxed text-foreground scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent selection:bg-primary/20 bg-card"
|
||||
>
|
||||
{#if activeDeploymentLogs}
|
||||
<pre
|
||||
class="whitespace-pre-wrap break-all">{activeDeploymentLogs}</pre>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-full items-center justify-center text-zinc-600 italic text-xs"
|
||||
class="flex h-full items-center justify-center text-muted-foreground italic text-xs"
|
||||
>
|
||||
<p>Select a deployment to view logs</p>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
(d) =>
|
||||
d.commit.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
d.status.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
d.ID.toString().includes(searchTerm),
|
||||
d.id.toString().includes(searchTerm),
|
||||
) || [],
|
||||
);
|
||||
|
||||
@@ -90,26 +90,26 @@
|
||||
<CardContent class="p-0">
|
||||
{#if project.deployments?.length}
|
||||
<div
|
||||
class="grid grid-cols-12 gap-4 px-4 py-3 border-b text-xs font-medium text-muted-foreground bg-muted/40 uppercase tracking-wider"
|
||||
class="grid grid-cols-12 gap-4 px-4 py-3 border-b text-xs font-medium text-muted-foreground bg-muted/40 uppercase tracking-wider text-center"
|
||||
>
|
||||
<div class="col-span-1">ID</div>
|
||||
<div class="col-span-2">Status</div>
|
||||
<div class="col-span-5">Commit</div>
|
||||
<div class="col-span-3">Date</div>
|
||||
<div class="col-span-1 text-right">Actions</div>
|
||||
<div class="col-span-4 md:col-span-1">ID</div>
|
||||
<div class="col-span-4 md:col-span-2">Status</div>
|
||||
<div class="hidden md:block col-span-5">Commit</div>
|
||||
<div class="hidden md:block col-span-3">Date</div>
|
||||
<div class="col-span-4 md:col-span-1">Actions</div>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-border/40">
|
||||
{#each filteredDeployments as deployment}
|
||||
{@const StatusIcon = getStatusIcon(deployment.status)}
|
||||
<div
|
||||
class="grid grid-cols-12 gap-4 px-4 py-2.5 items-center hover:bg-muted/30 transition-colors text-sm group"
|
||||
class="grid grid-cols-12 gap-4 px-4 py-2.5 items-center hover:bg-muted/30 transition-colors text-sm group text-center"
|
||||
>
|
||||
<div class="col-span-1 font-mono text-xs text-muted-foreground">
|
||||
#{deployment.ID}
|
||||
<div class="col-span-4 md:col-span-1 font-mono text-xs text-muted-foreground">
|
||||
#{deployment.id}
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
<div class="col-span-4 md:col-span-2 flex items-center justify-center gap-2">
|
||||
<StatusIcon
|
||||
class="h-4 w-4 {getStatusColor(
|
||||
deployment.status,
|
||||
@@ -125,15 +125,19 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-span-5 flex items-center gap-2 font-mono text-xs"
|
||||
class="hidden md:flex col-span-5 items-center justify-center gap-2 font-mono text-xs"
|
||||
>
|
||||
<GitCommit class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span
|
||||
class="bg-muted px-1.5 py-0.5 rounded border border-border/50 text-foreground/80"
|
||||
class="bg-muted px-2 py-0.5 rounded-md border border-border/50 text-foreground/80 font-mono"
|
||||
>
|
||||
{deployment.commit
|
||||
? deployment.commit.substring(0, 7)
|
||||
: "HEAD"}
|
||||
{deployment.commit === "HEAD"
|
||||
? "HEAD"
|
||||
: deployment.commit === "MANUAL"
|
||||
? "MANUAL"
|
||||
: deployment.commit === "WEBHOOK"
|
||||
? "WEBHOOK"
|
||||
: deployment.commit.substring(0, 7)}
|
||||
</span>
|
||||
<span
|
||||
class="text-muted-foreground truncate hidden md:inline-block max-w-[200px]"
|
||||
@@ -142,16 +146,16 @@
|
||||
? "Manual Redeploy"
|
||||
: deployment.commit === "WEBHOOK"
|
||||
? "Webhook Trigger"
|
||||
: "Git Push"}
|
||||
: "Git Push"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 text-xs text-muted-foreground">
|
||||
{new Date(deployment.CreatedAt).toLocaleString()}
|
||||
<div class="hidden md:block col-span-3 text-xs text-muted-foreground">
|
||||
{new Date(deployment.created_at).toLocaleString()}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-span-1 flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
class="col-span-4 md:col-span-1 flex items-center justify-center gap-1 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
{#if deployment.status === "live"}
|
||||
<Button
|
||||
@@ -169,7 +173,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
href={`/projects/${project.ID}?deployment=${deployment.ID}`}
|
||||
href={`/projects/${project.id}?deployment=${deployment.id}`}
|
||||
title="View Logs"
|
||||
>
|
||||
<Terminal class="h-3.5 w-3.5" />
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
EyeOff,
|
||||
Plus,
|
||||
Copy,
|
||||
Upload,
|
||||
} from "@lucide/svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
@@ -90,12 +91,12 @@
|
||||
}
|
||||
|
||||
loading = true;
|
||||
const success = await updateProjectEnv(project.ID.toString(), envMap);
|
||||
const success = await updateProjectEnv(project.id.toString(), envMap);
|
||||
loading = false;
|
||||
|
||||
if (success) {
|
||||
toast.success("Environment variables updated successfully");
|
||||
const res = await getProject(project.ID.toString());
|
||||
const res = await getProject(project.id.toString());
|
||||
if (res) {
|
||||
project = res;
|
||||
initEnvVars();
|
||||
@@ -107,10 +108,46 @@
|
||||
|
||||
function copyWebhook() {
|
||||
if (!project) return;
|
||||
const displayUrl = `http://localhost:8080/webhooks/trigger?project_id=${project.ID}`;
|
||||
const displayUrl = `http://localhost:8080/projects/${project.id}/webhook/${project.webhook_secret}`;
|
||||
navigator.clipboard.writeText(displayUrl);
|
||||
toast.success("Webhook URL copied");
|
||||
}
|
||||
|
||||
async function handleFileUpload(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const text = await file.text();
|
||||
const lines = text.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
|
||||
const [key, ...parts] = trimmed.split("=");
|
||||
if (key) {
|
||||
const value = parts.join("=");
|
||||
const existingIndex = tempEnvVars.findIndex(
|
||||
(e) => e.key === key.trim(),
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
tempEnvVars[existingIndex].value = value.replace(
|
||||
/^["'](.*)["']$/,
|
||||
"$1",
|
||||
); // remove quotes
|
||||
} else {
|
||||
tempEnvVars = [
|
||||
...tempEnvVars,
|
||||
{ key: key.trim(), value: value.replace(/^["'](.*)["']$/, "$1") },
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
isDirty = true;
|
||||
target.value = "";
|
||||
toast.success("Parsed .env file successfully");
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loading && !project}
|
||||
@@ -166,14 +203,32 @@
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
class="w-full h-11 border-dashed border-border/60 hover:bg-muted/50"
|
||||
onclick={addEnvVar}
|
||||
>
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
Add Variable
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="flex-1 h-11 border-dashed border-border/60 hover:bg-muted/50"
|
||||
onclick={addEnvVar}
|
||||
>
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
Add Variable
|
||||
</Button>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept=".env"
|
||||
class="hidden"
|
||||
id="env-upload"
|
||||
onchange={handleFileUpload}
|
||||
/>
|
||||
<Label
|
||||
for="env-upload"
|
||||
class="flex items-center justify-center px-4 h-11 rounded-md border border-dashed border-border/60 hover:bg-muted/50 cursor-pointer bg-background"
|
||||
>
|
||||
<Upload class="h-4 w-4 mr-2" />
|
||||
Upload .env
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="border-t px-4 flex justify-end">
|
||||
<Button onclick={saveEnvVars} disabled={!isDirty || loading} size="sm">
|
||||
@@ -199,7 +254,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
readonly
|
||||
value={`http://localhost:8080/webhooks/trigger?project_id=${project.ID}`}
|
||||
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}>
|
||||
@@ -207,15 +262,6 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-sm">Webhook Secret</Label>
|
||||
<Input
|
||||
type="password"
|
||||
readonly
|
||||
value={project.webhook_secret}
|
||||
class="bg-muted font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -88,8 +88,11 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>New Password</Label>
|
||||
type="password" bind:value={newPassword}
|
||||
required minlength={6}
|
||||
<Input
|
||||
type="password"
|
||||
bind:value={newPassword}
|
||||
required
|
||||
minlength={6}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" variant="secondary" disabled={loading}
|
||||
|
||||
Reference in New Issue
Block a user