Enviroment
This commit is contained in:
Generated
+102
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
@@ -1097,6 +1098,92 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
@@ -1942,6 +2029,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-previous": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-rect": {
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
|||||||
Vendored
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import testAuth from "../auth";
|
||||||
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import { getEnv, setEnv } from "@/core/server/application";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
const auth = await testAuth(req);
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: auth },
|
||||||
|
{ status: StatusCodes.UNAUTHORIZED }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(await getEnv());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||||
|
const auth = await testAuth(req);
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: auth },
|
||||||
|
{ status: StatusCodes.UNAUTHORIZED }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { appId, variables } = await req.json();
|
||||||
|
|
||||||
|
if (!appId || !variables) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Missing appId, variables in body" },
|
||||||
|
{ status: StatusCodes.BAD_REQUEST }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setEnv(appId, variables);
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "ok", success: true });
|
||||||
|
}
|
||||||
@@ -6,18 +6,32 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Plus, Trash2, Eye, EyeOff, Copy, Save, Lock } from "lucide-react";
|
import {
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
Copy,
|
||||||
|
Save,
|
||||||
|
Lock,
|
||||||
|
Loader2,
|
||||||
|
} from "lucide-react";
|
||||||
import { Env } from "@/lib/types";
|
import { Env } from "@/lib/types";
|
||||||
import { getEnv } from "@/core/application";
|
import { getEnv, setEnv } from "@/core/application";
|
||||||
|
import { AddVariableDialog } from "@/components/dialogs/AddVariable";
|
||||||
|
|
||||||
export default function EnvironmentPage() {
|
export default function EnvironmentPage() {
|
||||||
const [selectedApp, setSelectedApp] = useState("");
|
const [selectedApp, setSelectedApp] = useState("");
|
||||||
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
|
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
|
||||||
const [envGroups, setEnvGroups] = useState<Record<string, Env[]>>({});
|
const [envGroups, setEnvGroups] = useState<Record<string, Env>>({});
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetch = async () => {
|
const fetch = async () => {
|
||||||
setEnvGroups(await getEnv());
|
const env = await getEnv();
|
||||||
|
setEnvGroups(env);
|
||||||
|
const vars = Object.keys(env);
|
||||||
|
if (vars.length > 0) setSelectedApp(vars[0]);
|
||||||
};
|
};
|
||||||
fetch();
|
fetch();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -32,6 +46,17 @@ export default function EnvironmentPage() {
|
|||||||
return "•".repeat(Math.min(value.length, 24));
|
return "•".repeat(Math.min(value.length, 24));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
await setEnv(selectedApp, currentGroup.variables);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save:", error);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout
|
<DashboardLayout
|
||||||
title="Environment"
|
title="Environment"
|
||||||
@@ -47,21 +72,23 @@ export default function EnvironmentPage() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-2 flex gap-2 md:block overflow-x-auto">
|
<CardContent className="p-2 flex gap-2 md:block overflow-x-auto">
|
||||||
{Object.entries(envGroups).map(([key, variables]) => (
|
{Object.entries(envGroups).map(([id, variables]) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={id}
|
||||||
onClick={() => setSelectedApp(key)}
|
onClick={() => setSelectedApp(id)}
|
||||||
className={`
|
className={`
|
||||||
whitespace-nowrap md:w-full text-left px-3 py-2 text-sm font-mono transition-colors
|
whitespace-nowrap md:w-full text-left px-3 py-2 text-sm font-mono transition-colors
|
||||||
${
|
${
|
||||||
selectedApp === key
|
selectedApp === id
|
||||||
? "bg-secondary text-foreground"
|
? "bg-secondary text-foreground"
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{key}
|
{variables.name}
|
||||||
<Badge className="ml-2 text-[10px]">{variables.length}</Badge>
|
<Badge className="ml-2 text-[10px]">
|
||||||
|
{variables.variables.length}
|
||||||
|
</Badge>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -75,19 +102,32 @@ export default function EnvironmentPage() {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="font-mono">{selectedApp}</CardTitle>
|
<CardTitle className="font-mono">{selectedApp}</CardTitle>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
{currentGroup?.length} variables •{" "}
|
{currentGroup?.variables.length} variables •{" "}
|
||||||
{currentGroup?.filter((v) => v.secret).length} secrets
|
{currentGroup?.variables.filter((v) => v.secret).length}{" "}
|
||||||
|
secrets
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
<AddVariableDialog
|
||||||
<Plus className="w-4 h-4" />
|
selectedApp={selectedApp}
|
||||||
<span className="hidden sm:inline">Add Variable</span>
|
currentGroup={currentGroup}
|
||||||
</Button>
|
setEnvGroups={setEnvGroups}
|
||||||
<Button size="sm" className="gap-2">
|
/>
|
||||||
<Save className="w-4 h-4" />
|
<Button
|
||||||
<span className="hidden sm:inline">Save Changes</span>
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
{isSaving ? "Saving..." : "Save Changes"}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -102,7 +142,7 @@ export default function EnvironmentPage() {
|
|||||||
|
|
||||||
{/* Rows */}
|
{/* Rows */}
|
||||||
<div className="divide-y divide-border md:divide-none">
|
<div className="divide-y divide-border md:divide-none">
|
||||||
{currentGroup?.map((variable) => (
|
{currentGroup?.variables.map((variable) => (
|
||||||
<div
|
<div
|
||||||
key={variable.key}
|
key={variable.key}
|
||||||
className="
|
className="
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { Env, EnvVariable } from "@/lib/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedApp: string;
|
||||||
|
currentGroup: Env;
|
||||||
|
setEnvGroups: React.Dispatch<React.SetStateAction<Record<string, Env>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddVariableDialog({
|
||||||
|
selectedApp,
|
||||||
|
currentGroup,
|
||||||
|
setEnvGroups,
|
||||||
|
}: Props) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const [key, setKey] = React.useState("");
|
||||||
|
const [value, setValue] = React.useState("");
|
||||||
|
const [secret, setSecret] = React.useState(false);
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
if (!key) return;
|
||||||
|
const newVariable: EnvVariable = { key, value, secret };
|
||||||
|
setEnvGroups((env) => ({
|
||||||
|
...env,
|
||||||
|
[selectedApp]: {
|
||||||
|
...currentGroup,
|
||||||
|
variables: [...currentGroup.variables, newVariable],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
setKey("");
|
||||||
|
setValue("");
|
||||||
|
setSecret(false);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Add Variable</span>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add Environment Variable</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter the variable name, value, and whether it should be secret.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<label className="text-sm font-medium">Variable Name</label>
|
||||||
|
<Input
|
||||||
|
placeholder="PORT"
|
||||||
|
value={key}
|
||||||
|
onChange={(e) => setKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<label className="text-sm font-medium">Value</label>
|
||||||
|
<Input
|
||||||
|
placeholder="3000"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="secret"
|
||||||
|
checked={secret}
|
||||||
|
onCheckedChange={(checked) => setSecret(!!checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="secret" className="text-sm font-medium">
|
||||||
|
Secret
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={handleAdd}>Add Variable</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="grid place-content-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
+18
-4
@@ -3,7 +3,7 @@
|
|||||||
import { verifySession } from "./auth/session";
|
import { verifySession } from "./auth/session";
|
||||||
import { getNodes } from "./node";
|
import { getNodes } from "./node";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Env, ImageApp } from "@/lib/types";
|
import { Env, EnvVariable, ImageApp } from "@/lib/types";
|
||||||
|
|
||||||
export async function deployNewApp(name: string, repo: string, nodeId: string) {
|
export async function deployNewApp(name: string, repo: string, nodeId: string) {
|
||||||
if (await verifySession()) {
|
if (await verifySession()) {
|
||||||
@@ -42,7 +42,7 @@ export async function getApplications(): Promise<ImageApp[]> {
|
|||||||
).flat();
|
).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEnv(): Promise<Record<string, Env[]>> {
|
export async function getEnv(): Promise<Record<string, Env>> {
|
||||||
const nodes = await getNodes();
|
const nodes = await getNodes();
|
||||||
|
|
||||||
const envRecords = await Promise.all(
|
const envRecords = await Promise.all(
|
||||||
@@ -50,11 +50,25 @@ export async function getEnv(): Promise<Record<string, Env[]>> {
|
|||||||
const response = await axios.get(`http://${n.ip}:3000/api/env`, {
|
const response = await axios.get(`http://${n.ip}:3000/api/env`, {
|
||||||
headers: { Authorization: `ApiKey ${n.key}` },
|
headers: { Authorization: `ApiKey ${n.key}` },
|
||||||
});
|
});
|
||||||
return response.data as Record<string, Env[]>;
|
return response.data as Record<string, Env>;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const merged: Record<string, Env[]> = Object.assign({}, ...envRecords);
|
const merged: Record<string, Env> = Object.assign({}, ...envRecords);
|
||||||
|
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setEnv(appId: string, variables: EnvVariable[]) {
|
||||||
|
const nodes = await getNodes();
|
||||||
|
|
||||||
|
nodes.forEach(async (n) => {
|
||||||
|
await axios.post(
|
||||||
|
`http://${n.ip}:3000/api/env`,
|
||||||
|
{ appId, variables },
|
||||||
|
{
|
||||||
|
headers: { Authorization: `ApiKey ${n.key}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import util from "util";
|
import util from "util";
|
||||||
import { Env, ImageApp } from "@/lib/types";
|
import { Env, EnvVariable, ImageApp } from "@/lib/types";
|
||||||
import { CPUusage } from "@/lib/server/calc";
|
import { CPUusage } from "@/lib/server/calc";
|
||||||
|
|
||||||
const docker = new Docker();
|
const docker = new Docker();
|
||||||
@@ -135,7 +135,7 @@ export async function deployNewApp(name: string, repo: string, nodeId: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEnv(): Promise<Record<string, Env[]>> {
|
export async function getEnv(): Promise<Record<string, Env>> {
|
||||||
const apps = fs.readdirSync(DATA_DIR);
|
const apps = fs.readdirSync(DATA_DIR);
|
||||||
|
|
||||||
return apps.reduce((acc, appId) => {
|
return apps.reduce((acc, appId) => {
|
||||||
@@ -144,9 +144,23 @@ export async function getEnv(): Promise<Record<string, Env[]>> {
|
|||||||
|
|
||||||
const envPath = path.join(DATA_DIR, appId, "env.json");
|
const envPath = path.join(DATA_DIR, appId, "env.json");
|
||||||
if (fs.existsSync(envPath)) {
|
if (fs.existsSync(envPath)) {
|
||||||
const envData: Env[] = JSON.parse(fs.readFileSync(envPath, "utf-8"));
|
const envData: EnvVariable[] = JSON.parse(
|
||||||
acc[name] = envData;
|
fs.readFileSync(envPath, "utf-8")
|
||||||
|
);
|
||||||
|
acc[appId] = { variables: envData, id: appId, name };
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, Env[]>);
|
}, {} as Record<string, Env>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setEnv(appId: string, variables: EnvVariable[]) {
|
||||||
|
const apps = fs.readdirSync(DATA_DIR);
|
||||||
|
|
||||||
|
apps.forEach((id) => {
|
||||||
|
if (id === appId) {
|
||||||
|
const envPath = path.join(DATA_DIR, appId, "env.json");
|
||||||
|
|
||||||
|
fs.writeFileSync(envPath, JSON.stringify(variables));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -82,8 +82,14 @@ export interface Log {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Env {
|
export interface EnvVariable {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Env {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
variables: EnvVariable[];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user