App dialogs
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"install": "sh test"
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
+1
-1
@@ -185,7 +185,7 @@
|
|||||||
--popover-foreground: oklch(0.9108 0.0427 269.5516);
|
--popover-foreground: oklch(0.9108 0.0427 269.5516);
|
||||||
--primary: oklch(0.7182 0.1105 239.784);
|
--primary: oklch(0.7182 0.1105 239.784);
|
||||||
--primary-foreground: oklch(0.3233 0.1025 253.8852);
|
--primary-foreground: oklch(0.3233 0.1025 253.8852);
|
||||||
--secondary: oklch(0.5544 0.0407 257.4166);
|
--secondary: oklch(0.38 0.06 258);
|
||||||
--secondary-foreground: oklch(0.9288 0.0126 255.5078);
|
--secondary-foreground: oklch(0.9288 0.0126 255.5078);
|
||||||
--muted: oklch(0.3923 0.0613 254.3914);
|
--muted: oklch(0.3923 0.0613 254.3914);
|
||||||
--muted-foreground: oklch(0.7584 0.0419 263.598);
|
--muted-foreground: oklch(0.7584 0.0419 263.598);
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Session from "@/lib/session";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export default function AddAppDialog({
|
||||||
|
session,
|
||||||
|
}: {
|
||||||
|
session: Session | null;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [ip, setIp] = useState("");
|
||||||
|
const [location, setLocation] = useState<string | undefined>(undefined);
|
||||||
|
const [coordinates, setCoordinates] = useState<[number, number] | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger
|
||||||
|
className={cn(buttonVariants({ variant: "default" }))}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<Plus className="m-0 p-0" /> New
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add Application</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Label className="mt-2">
|
||||||
|
Application Name<span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Sentryx chat, My Project, etc"
|
||||||
|
required
|
||||||
|
onInput={(e) => setName(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label className="mt-2">
|
||||||
|
Application service<span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="my-app.service"
|
||||||
|
required
|
||||||
|
onInput={(e) => setIp(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={name == "" || ip == ""}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add App
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -87,7 +87,7 @@ export default function AppComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer !select-none border-accent border-2"
|
className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer !select-none border-secondary border-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
redirect(`/servers/${index}`);
|
redirect(`/servers/${index}`);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import AppComponent from "./AppComponent";
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import AddAppDialog from "./AddAppDialog";
|
||||||
|
import InstallAppDialog from "./IntallAppDialog";
|
||||||
|
|
||||||
interface Server2 extends Server {
|
interface Server2 extends Server {
|
||||||
services: Service[],
|
services: Service[],
|
||||||
@@ -40,12 +42,13 @@ export default function ClientPage({ serverId }: { serverId: string }) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex">
|
<CardHeader className="flex">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Sentryx Apps</CardTitle>
|
<CardTitle>Sentryx Applications</CardTitle>
|
||||||
<CardDescription>Your Sentryx apps on this server</CardDescription>
|
<CardDescription>Your Sentryx apps on this server</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button className="ml-auto" onClick={() => {
|
<div className="ml-auto w-max flex gap-3">
|
||||||
toast.warning("Functionality not added yet");
|
<InstallAppDialog session={session} />
|
||||||
}}>Add</Button>
|
<AddAppDialog session={session} />
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
|
import { HardDriveDownload, Plus } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Session from "@/lib/session";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { FileUpload } from "@/components/FileUpload";
|
||||||
|
|
||||||
|
export default function InstallAppDialog({
|
||||||
|
session,
|
||||||
|
}: {
|
||||||
|
session: Session | null;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [script, setScript] = useState<{ install?: string } | undefined>({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger
|
||||||
|
className={cn(buttonVariants({ variant: "secondary" }))}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<HardDriveDownload className="m-0 p-0" /> Install
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Install Application</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Label className="mt-2">
|
||||||
|
Application Name<span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Sentryx chat, My Project, etc"
|
||||||
|
required
|
||||||
|
onInput={(e) => setName(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label className="mt-2">
|
||||||
|
Application script<span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<FileUpload setSelected={(contents) => {
|
||||||
|
try {
|
||||||
|
setScript(JSON.parse(contents))
|
||||||
|
console.log(contents)
|
||||||
|
} catch {
|
||||||
|
setScript(undefined)
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={name == "" || !script || !(script.install)}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Install
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useState, useRef } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { cn } from "@/lib/utils" // optional utility for combining classes
|
||||||
|
|
||||||
|
export function FileUpload({setSelected} : {setSelected: (contents: string) => void}) {
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
const [fileName, setFileName] = useState<string | null>(null)
|
||||||
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
const [isUnsupported, setIsUnsupported] = useState(false)
|
||||||
|
|
||||||
|
const handleFiles = (files: FileList | null) => {
|
||||||
|
const file = files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
setIsUnsupported(!file.name.endsWith('.json'));
|
||||||
|
if (isUnsupported) return
|
||||||
|
|
||||||
|
setFileName(file.name)
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
setSelected(e.target?.result as string)
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsDragging(false)
|
||||||
|
handleFiles(e.dataTransfer.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
onChange={(e) => handleFiles(e.target.files)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsDragging(true)
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setIsDragging(false)}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
className={cn(
|
||||||
|
"flex h-32 cursor-pointer items-center justify-center rounded-md border-2 border-dashed transition-colors",
|
||||||
|
isDragging ? "border-primary bg-muted" : "border-muted",
|
||||||
|
isUnsupported && "border-destructive"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{isUnsupported ? "Unsupported file type" : (fileName || "Click or drag file to upload")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user