Skip to content

Commit

Permalink
fix merge
Browse files Browse the repository at this point in the history
  • Loading branch information
zacharyblasczyk committed Sep 25, 2024
2 parents e431788 + badfc78 commit 6ad30d5
Show file tree
Hide file tree
Showing 37 changed files with 28,899 additions and 61 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* text=auto eol=lf

github/**/index.js -diff linguist-generated=true
4 changes: 2 additions & 2 deletions apps/event-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
"@types/semver": "^7.5.8",
"eslint": "catalog:",
"prettier": "catalog:",
"tsx": "^4.11.0",
"typescript": "^5.6.2"
"tsx": "catalog:",
"typescript": "catalog:"
},
"prettier": "@ctrlplane/prettier-config"
}
4 changes: 2 additions & 2 deletions apps/job-policy-checker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
"@ctrlplane/tsconfig": "workspace:*",
"eslint": "catalog:",
"prettier": "catalog:",
"tsx": "^4.11.0",
"typescript": "^5.6.2"
"tsx": "catalog:",
"typescript": "catalog:"
},
"prettier": "@ctrlplane/prettier-config"
}
2 changes: 2 additions & 0 deletions apps/webservice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"geist": "^1.3.0",
"http-status": "^1.7.4",
"immer": "^10.1.1",
"js-yaml": "^4.1.0",
"lz-string": "^1.5.0",
"match-sorter": "^6.3.4",
"murmurhash": "^2.0.1",
Expand Down Expand Up @@ -78,6 +79,7 @@
"@ctrlplane/tailwind-config": "workspace:*",
"@ctrlplane/tsconfig": "workspace:*",
"@types/dagre": "^0.7.52",
"@types/js-yaml": "^4.0.9",
"@types/node": "catalog:node20",
"@types/randomcolor": "^0.5.9",
"@types/react": "catalog:react18",
Expand Down
11 changes: 11 additions & 0 deletions apps/webservice/src/app/[workspaceSlug]/SidebarCreateMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { CreateDeploymentDialog } from "./_components/CreateDeployment";
import { CreateReleaseDialog } from "./_components/CreateRelease";
import { CreateSystemDialog } from "./_components/CreateSystem";
import { CreateTargetDialog } from "./_components/CreateTarget";

export const SidebarCreateMenu: React.FC<{
workspace: Workspace;
Expand Down Expand Up @@ -64,6 +65,16 @@ export const SidebarCreateMenu: React.FC<{
<DropdownMenuGroup>
<DropdownMenuItem>Execute Runbook</DropdownMenuItem>
</DropdownMenuGroup>

<DropdownMenuSeparator />

<DropdownMenuGroup>
<CreateTargetDialog {...props} onSuccess={() => setOpen(false)}>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
Bootstrap Target
</DropdownMenuItem>
</CreateTargetDialog>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
254 changes: 254 additions & 0 deletions apps/webservice/src/app/[workspaceSlug]/_components/CreateTarget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
"use client";

import type { Workspace } from "@ctrlplane/db/schema";
import type React from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { IconX } from "@tabler/icons-react";
import yaml from "js-yaml";
import { z } from "zod";

import { Button } from "@ctrlplane/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@ctrlplane/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormRootError,
useFieldArray,
useForm,
} from "@ctrlplane/ui/form";
import { Input } from "@ctrlplane/ui/input";
import { Label } from "@ctrlplane/ui/label";

import { api } from "~/trpc/react";
import { TargetConfigEditor } from "./TargetConfigEditor";

const createTargetSchema = z.object({
name: z.string(),
kind: z.string(),
identifier: z.string().min(4),
version: z.string(),
config: z.string().refine((val) => {
try {
const output = yaml.load(val);
const isValidRecord = z.record(z.any()).safeParse(output).success;
return isValidRecord;
} catch {
return false;
}
}, "Config must be valid YAML Object"),
metadata: z.array(z.object({ key: z.string(), value: z.string() })),
});

const defaultValues = {
name: "",
identifier: "",
kind: "",
version: "",
metadata: [{ key: "", value: "" }],
config: "",
};

export const CreateTargetDialog: React.FC<{
children: React.ReactNode;
workspace: Workspace;
onSuccess?: () => void;
}> = ({ children, workspace, onSuccess }) => {
const [open, setOpen] = useState(false);

const form = useForm({
schema: createTargetSchema,
defaultValues,
mode: "onSubmit",
});

useEffect(() => {
if (!open) form.reset();
}, [form, open]);

const router = useRouter();
const create = api.target.create.useMutation();
const onSubmit = form.handleSubmit(async (data) => {
const config = yaml.load(data.config) as Record<string, any>;
const target = await create.mutateAsync({
...data,
config,
metadata: Object.fromEntries(
data.metadata.map(({ key, value }) => [key, value]),
),
workspaceId: workspace.id,
});

const query = new URLSearchParams(window.location.search);
query.set("target_id", target.id);
router.replace(`?${query.toString()}`);
router.refresh();
onSuccess?.();
});

const { fields, append, remove } = useFieldArray({
name: "metadata",
control: form.control,
});

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<Form {...form}>
<form onSubmit={onSubmit} className="space-y-3">
<DialogHeader>
<DialogTitle>Bootstrap Target</DialogTitle>
<DialogDescription>
Targets are typically created automatically through scanners
that discover and register new targets in your infrastructure.
However, you can manually bootstrap a target if needed.
</DialogDescription>
</DialogHeader>

<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="my-target" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="identifier"
render={({ field }) => (
<FormItem>
<FormLabel>Identifier</FormLabel>
<FormControl>
<Input placeholder="mycompany-my-target" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="version"
render={({ field }) => (
<FormItem>
<FormLabel>Version</FormLabel>
<FormControl>
<Input placeholder="mycompany/v1" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem>
<FormLabel>Kind</FormLabel>
<FormControl>
<Input placeholder="MyCustomTarget" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>

<FormField
control={form.control}
name="config"
render={({ field: { onChange, value } }) => (
<FormItem>
<FormLabel>Config</FormLabel>
<FormControl>
<TargetConfigEditor value={value} onChange={onChange} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<div>
<div className="pb-2">
<Label>Metadata</Label>
</div>
{fields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`metadata.${index}`}
render={({ field: { onChange, value } }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-4">
<Input
value={value.key}
onChange={(e) =>
onChange({ ...value, key: `${e.target.value}` })
}
/>
<Input
value={value.value}
onChange={(e) =>
onChange({ ...value, value: `${e.target.value}` })
}
/>
<Button
variant="ghost"
size="icon"
className="h-4 w-4"
onClick={() => remove(index)}
>
<IconX />
</Button>
</div>
</FormControl>
</FormItem>
)}
/>
))}
<Button
type="button"
variant="outline"
size="sm"
className="mt-4"
onClick={() => append({ key: "", value: "" })}
>
Add Metadata
</Button>
</div>

<FormRootError />
<DialogFooter>
<Button type="submit" disabled={create.isPending}>
Create Target
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ export const DeploymentOptionsDropdown: React.FC<{
<DropdownMenuGroup>
<EditDeploymentDialog {...props}>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<IconEdit className="mr-2" />
<IconEdit className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
</EditDeploymentDialog>
<DropdownMenuItem onSelect={() => setDeleteDialogOpen(true)}>
<IconTrash className="mr-2" />
<IconTrash className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ export const JobTableStatusIcon: React.FC<{ status?: schema.JobStatus }> = ({
</div>
);

return <IconClock className="text-neutral-400" />;
return <IconClock className="h-4 w-4 text-neutral-400" />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import React from "react";
import Editor, { loader } from "@monaco-editor/react";
import colors from "tailwindcss/colors";

import { Card } from "@ctrlplane/ui/card";

loader.init().then((monaco) => {
monaco.editor.defineTheme("vs-dark-custom", {
base: "vs-dark",
inherit: true,
rules: [],
colors: {
"editor.background": colors.neutral[950],
},
});
});

export const TargetConfigEditor: React.FC<{
value: string;
onChange?: (v: string) => void;
readOnly?: boolean;
}> = ({ readOnly, value, onChange }) => {
return (
<Card>
<div className="p-2">
<Editor
height="200px"
defaultLanguage="yaml"
value={value}
theme="vs-dark-custom"
onChange={(v) => onChange?.(v ?? "")}
options={{ readOnly }}
/>
</div>
</Card>
);
};
Loading

0 comments on commit 6ad30d5

Please sign in to comment.