diff --git a/src/app/dashboard/dev/page.tsx b/src/app/dashboard/dev/page.tsx index 9fcc4b2..aa91290 100644 --- a/src/app/dashboard/dev/page.tsx +++ b/src/app/dashboard/dev/page.tsx @@ -20,6 +20,7 @@ import { ArrowRight, ChevronDown, Building2, + Upload, } from "lucide-react"; import { Skeleton, SkeletonText, SkeletonCard } from "@/components/Skeleton"; @@ -449,13 +450,21 @@ export default function DevDashboardPage() { your repositories. The app will automatically detect your Bedrock plugins, build them, and publish them to the marketplace.

- - Install GitHub App{" "} - - +
+ + Install GitHub App{" "} + + + + Upload Proprietary Plugin + +
); @@ -473,6 +482,12 @@ export default function DevDashboardPage() { Manage your GitHub repositories and CI/CD pipelines

+ + Upload Proprietary Plugin + {/* Stats */} diff --git a/src/app/dashboard/upload/[slug]/version/page.tsx b/src/app/dashboard/upload/[slug]/version/page.tsx new file mode 100644 index 0000000..9dc502d --- /dev/null +++ b/src/app/dashboard/upload/[slug]/version/page.tsx @@ -0,0 +1,232 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useSession } from "next-auth/react"; +import { useRouter, useParams } from "next/navigation"; +import { ArrowLeft, Upload, AlertTriangle, CheckCircle } from "lucide-react"; + +export default function UploadVersionPage() { + const { data: session } = useSession(); + const router = useRouter(); + const params = useParams(); + const slug = params.slug as string; + + const [plugin, setPlugin] = useState(null); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(""); + + const [artifact, setArtifact] = useState(null); + const [artifactLinux, setArtifactLinux] = useState(null); + const [artifactWin, setArtifactWin] = useState(null); + + useEffect(() => { + const fetchPlugin = async () => { + try { + const apiUrl = + process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; + const token = (session?.user as any)?.apiToken; + const res = await fetch(`${apiUrl}/api/v1/plugins/${slug}`, { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); + const json = await res.json(); + if (json.success) { + setPlugin(json.data); + } else { + setError("Plugin not found"); + } + } catch { + setError("Failed to load plugin"); + } finally { + setLoading(false); + } + }; + if (session) fetchPlugin(); + }, [session, slug]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + if (plugin?.pluginType === "PYTHON" && !artifact) { + setError("A .whl artifact file is required."); + return; + } + if (plugin?.pluginType === "CPP") { + if (!artifactLinux) { + setError("A Linux .so artifact file is required."); + return; + } + if (!artifactWin) { + setError("A Windows .dll artifact file is required."); + return; + } + } + + setSubmitting(true); + try { + const formData = new FormData(); + if (plugin?.pluginType === "PYTHON" && artifact) { + formData.append("artifact", artifact); + } + if (plugin?.pluginType === "CPP") { + if (artifactLinux) formData.append("artifact_linux", artifactLinux); + if (artifactWin) formData.append("artifact_win", artifactWin); + } + + const apiToken = (session?.user as any)?.apiToken; + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"}/api/v1/upload/plugin/${slug}/version`, + { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + body: formData, + }, + ); + const data = await res.json(); + if (data.success) { + router.push(`/builds/${data.data.build.id}/submit`); + } else { + setError(data.error || "Upload failed"); + } + } catch { + setError("An unexpected error occurred"); + } finally { + setSubmitting(false); + } + }; + + if (!session) + return
Please sign in.
; + + if (loading) { + return ( +
+
+ Loading plugin... +
+
+ ); + } + + if (error && !plugin) { + return ( +
+
{error}
+
+ ); + } + + const nextBuildNumber = (plugin?.builds?.length || 0) + 1; + + return ( +
+ + +
+

Upload New Version

+
+

+ {plugin?.displayName} +

+

+ Upload a new pre-built artifact. After uploading you'll be + taken to the submission form to set the version number, changelog, + and submit for review. +

+
+ +
+ {plugin?.pluginType === "PYTHON" ? ( +
+ +

+ Upload your pre-built .whl file (max 100MB). +

+ setArtifact(e.target.files?.[0] || null)} + className="w-full px-3 py-2 rounded-md border border-border bg-surface-secondary text-text-primary outline-none file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:bg-accent file:text-white file:text-sm file:cursor-pointer" + /> + {artifact && ( +

+ Selected: {artifact.name} ( + {(artifact.size / 1024 / 1024).toFixed(1)} MB) +

+ )} +
+ ) : ( +
+
+ +

+ Upload the Linux .so artifact (max 100MB). +

+ + setArtifactLinux(e.target.files?.[0] || null) + } + className="w-full px-3 py-2 rounded-md border border-border bg-surface-secondary text-text-primary outline-none file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:bg-accent file:text-white file:text-sm file:cursor-pointer" + /> + {artifactLinux && ( +

+ Selected: {artifactLinux.name} ( + {(artifactLinux.size / 1024 / 1024).toFixed(1)} MB) +

+ )} +
+
+ +

+ Upload the Windows .dll artifact (max 100MB). +

+ setArtifactWin(e.target.files?.[0] || null)} + className="w-full px-3 py-2 rounded-md border border-border bg-surface-secondary text-text-primary outline-none file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:bg-accent file:text-white file:text-sm file:cursor-pointer" + /> + {artifactWin && ( +

+ Selected: {artifactWin.name} ( + {(artifactWin.size / 1024 / 1024).toFixed(1)} MB) +

+ )} +
+
+ )} + +
+ +
+
+ + {error && ( +
+ {error} +
+ )} +
+
+ ); +} diff --git a/src/app/dashboard/upload/page.tsx b/src/app/dashboard/upload/page.tsx new file mode 100644 index 0000000..f9b8829 --- /dev/null +++ b/src/app/dashboard/upload/page.tsx @@ -0,0 +1,396 @@ +"use client"; + +import { useState } from "react"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { + CheckCircle, + AlertTriangle, + ArrowLeft, + Upload, + Lock, +} from "lucide-react"; +import { PLUGIN_CATEGORIES } from "@/lib/constants"; + +export default function UploadPluginPage() { + const { data: session } = useSession(); + const router = useRouter(); + + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(""); + + const [name, setName] = useState(""); + const [displayName, setDisplayName] = useState(""); + const [description, setDescription] = useState(""); + const [longDescription, setLongDescription] = useState(""); + const [pluginType, setPluginType] = useState<"PYTHON" | "CPP">("PYTHON"); + const [selectedCategories, setSelectedCategories] = useState([]); + const [keywords, setKeywords] = useState(""); + const [iconUrl, setIconUrl] = useState(""); + + const [artifact, setArtifact] = useState(null); + const [artifactLinux, setArtifactLinux] = useState(null); + const [artifactWin, setArtifactWin] = useState(null); + + const toggleCategory = (cat: string) => { + if (selectedCategories.includes(cat)) { + setSelectedCategories((prev) => prev.filter((c) => c !== cat)); + } else { + if (selectedCategories.length >= 5) { + setError("You can only select up to 5 categories."); + return; + } + setError(""); + setSelectedCategories((prev) => [...prev, cat]); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + if (!name.match(/^[a-z0-9][a-z0-9-]{0,62}$/)) { + setError( + "Plugin name must be 1-63 lowercase alphanumeric characters or hyphens, starting with a letter or number.", + ); + return; + } + if (!displayName.trim()) { + setError("Display name is required."); + return; + } + if (!description.trim() || description.length > 100) { + setError("Description is required and must be at most 100 characters."); + return; + } + if (selectedCategories.length === 0) { + setError("Please select at least one category."); + return; + } + + if (pluginType === "PYTHON" && !artifact) { + setError("A .whl artifact file is required for Python plugins."); + return; + } + if (pluginType === "CPP") { + if (!artifactLinux) { + setError("A Linux .so artifact file is required for C++ plugins."); + return; + } + if (!artifactWin) { + setError("A Windows .dll artifact file is required for C++ plugins."); + return; + } + } + + setSubmitting(true); + try { + const formData = new FormData(); + formData.append("name", name); + formData.append("displayName", displayName); + formData.append("description", description); + if (longDescription) formData.append("longDescription", longDescription); + formData.append("pluginType", pluginType); + if (selectedCategories.length > 0) + formData.append("tags", selectedCategories.join(",")); + if (keywords) formData.append("keywords", keywords); + if (iconUrl) formData.append("iconUrl", iconUrl); + + if (pluginType === "PYTHON" && artifact) { + formData.append("artifact", artifact); + } + if (pluginType === "CPP") { + if (artifactLinux) formData.append("artifact_linux", artifactLinux); + if (artifactWin) formData.append("artifact_win", artifactWin); + } + + const apiToken = (session?.user as any)?.apiToken; + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"}/api/v1/upload/plugin`, + { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + body: formData, + }, + ); + const data = await res.json(); + if (data.success) { + router.push(`/builds/${data.data.build.id}/submit`); + } else { + setError(data.error || "Upload failed"); + } + } catch { + setError("An unexpected error occurred"); + } finally { + setSubmitting(false); + } + }; + + if (!session) + return
Please sign in.
; + + return ( +
+ + +
+

Upload Proprietary Plugin

+
+

+ Proprietary Plugin Upload +

+

+ Upload pre-built artifacts directly. The license will be + automatically set to "Proprietary". Your plugin will go + through VirusTotal scanning and admin review before being published. +

+
+ +
+

+ About this plugin... +

+ +
+ +

+ Unique identifier. Lowercase letters, numbers, and hyphens only. +

+ setName(e.target.value.toLowerCase())} + placeholder="my-awesome-plugin" + pattern="[a-z0-9][a-z0-9-]{0,62}" + className="w-full px-3 py-2 rounded-md border border-border bg-surface-secondary text-text-primary outline-none transition-all duration-150 focus:border-accent" + /> +
+ +
+ +

+ The clean name shown on the marketplace. +

+ setDisplayName(e.target.value)} + maxLength={64} + className="w-full px-3 py-2 rounded-md border border-border bg-surface-secondary text-text-primary outline-none transition-all duration-150 focus:border-accent" + /> +
+ +
+ +

+ A catchy, one-sentence summary (max 100 chars). +

+ setDescription(e.target.value)} + maxLength={100} + placeholder="A brief summary of what your plugin does..." + className="w-full px-3 py-2 rounded-md border border-border bg-surface-secondary text-text-primary outline-none transition-all duration-150 focus:border-accent" + /> +
+ +
+ +

+ Explain features, configuration, and usage. +

+