From 27e9e2d9411ffa05bbf7ef1730e4753cad03df15 Mon Sep 17 00:00:00 2001 From: Fayorg Date: Wed, 8 May 2024 21:56:23 +0200 Subject: [PATCH] add: specs & database status --- .../[workspace]/databases/database-column.tsx | 13 +- .../databases/database-new-form.tsx | 114 ++++++++---------- .../[workspace]/databases/database-table.tsx | 50 +++++++- app/(deploy)/[workspace]/databases/page.tsx | 13 +- .../deploy/database/provider/postgres.tsx | 101 ++++++++++++++++ components/ui/slider.tsx | 18 +++ lib/deploy/database-config.ts | 80 ++++++++++++ lib/deploy/database.ts | 68 ++++++++++- package-lock.json | 34 ++++++ package.json | 1 + utils/name-generator.ts | 27 +++++ utils/password-generator.ts | 33 +++++ 12 files changed, 472 insertions(+), 80 deletions(-) create mode 100644 components/deploy/database/provider/postgres.tsx create mode 100644 components/ui/slider.tsx create mode 100644 lib/deploy/database-config.ts create mode 100644 utils/name-generator.ts create mode 100644 utils/password-generator.ts diff --git a/app/(deploy)/[workspace]/databases/database-column.tsx b/app/(deploy)/[workspace]/databases/database-column.tsx index dc51413..a006c79 100644 --- a/app/(deploy)/[workspace]/databases/database-column.tsx +++ b/app/(deploy)/[workspace]/databases/database-column.tsx @@ -3,12 +3,12 @@ import { Database } from '@prisma/client'; import { ColumnDef } from '@tanstack/react-table'; import Image from 'next/image'; -import { databaseProviders } from './database-new-form'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { Button } from '@/components/ui/button'; -import { EllipsisVertical } from 'lucide-react'; +import { databaseProviders } from '@/lib/deploy/database-config'; +import { cn } from '@/lib/utils'; -interface IDatabase extends Pick {} +interface IDatabase extends Pick { + status: any; +} export const columns: ColumnDef[] = [ { @@ -16,8 +16,9 @@ export const columns: ColumnDef[] = [ header: 'Provider', cell(props) { return ( -
+
pro.id.toUpperCase() === props.getValue())[0]?.image?.src} alt={databaseProviders.filter((pro) => pro.id.toUpperCase() === props.getValue())[0]?.image?.alt} width={32} height={32} /> +
); }, diff --git a/app/(deploy)/[workspace]/databases/database-new-form.tsx b/app/(deploy)/[workspace]/databases/database-new-form.tsx index 0129449..1ba85c9 100644 --- a/app/(deploy)/[workspace]/databases/database-new-form.tsx +++ b/app/(deploy)/[workspace]/databases/database-new-form.tsx @@ -1,5 +1,6 @@ 'use client'; +import { PostgresSpecsPage } from '@/components/deploy/database/provider/postgres'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -8,6 +9,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { useWorkspace } from '@/hooks/useWorkspace'; import { deployDatabase } from '@/lib/deploy/database'; +import { databaseProviders, defaultDatabaseConfiguration, IDatabaseConfiguration } from '@/lib/deploy/database-config'; import { cn } from '@/lib/utils'; import { Plus } from 'lucide-react'; import Image from 'next/image'; @@ -18,25 +20,6 @@ interface DatabaseNewSteps { display: string; } -interface IDatabaseProvider { - id: string; - display: string; - image: { - alt: string; - src: string; - }; -} - -export interface IDatabaseConfig { - workspaceId: string; - name: string; - provider: IDatabaseProvider; - user: { - username: string; - password: string; - }; -} - const steps: DatabaseNewSteps[] = [ { display: 'Database Type', @@ -52,55 +35,26 @@ const steps: DatabaseNewSteps[] = [ }, ]; -export const databaseProviders: IDatabaseProvider[] = [ - { - id: 'vitess', - display: 'Vitess', - image: { - alt: 'Vitess', - src: '/vitess.png', - }, - }, - { - id: 'redis', - display: 'Redis', - image: { - alt: 'Redis', - src: '/redis.svg', - }, - }, - { - id: 'postgres', - display: 'Postgres', - image: { - alt: 'Postgres', - src: '/postgres.png', - }, - }, -]; - export default function DatabaseNewForm() { const [open, setOpen] = useState(false); const [currentSteps, setCurrentSteps] = useState(1); const router = useRouter(); - - // TODO: Generate all data randomly, but leave the user the choice to modify const { id } = useWorkspace(); - const defaultDatabaseConfig: IDatabaseConfig = { - workspaceId: id, - name: 'my-new-cool-db', - provider: databaseProviders[0], - user: { - username: 'my-new-super-user', - password: 'a-super-strong-generated-password', - }, - }; - const [databaseConfig, setDatabaseConfig] = useState(defaultDatabaseConfig); + const [databaseConfig, setDatabaseConfig] = useState(defaultDatabaseConfiguration(id)); return ( - setOpen((open) => !open)}> + { + setOpen((open) => !open); + if (!open) { + setDatabaseConfig(defaultDatabaseConfiguration(id)); + setCurrentSteps(1); + } + }} + > + +
+

Deleting a database if irreversible. This action cannot be undone.

+ +
+ +
+ + diff --git a/app/(deploy)/[workspace]/databases/page.tsx b/app/(deploy)/[workspace]/databases/page.tsx index f55bb9a..22ce8ee 100644 --- a/app/(deploy)/[workspace]/databases/page.tsx +++ b/app/(deploy)/[workspace]/databases/page.tsx @@ -3,6 +3,7 @@ import DatabaseNewForm from './database-new-form'; import prisma from '@/lib/prisma'; import { DatabaseTable } from './database-table'; import { columns } from './database-column'; +import { getDatabase } from '@/lib/deploy/database'; export default async function Databases({ params }: { params: { workspace: string } }) { const workspaceSlug = params.workspace; @@ -15,6 +16,16 @@ export default async function Databases({ params }: { params: { workspace: strin }, }); + const status = await Promise.all( + workspace.Database.map(async (database) => { + const res = await getDatabase({ + id: database.id, + workspaceId: workspace.id, + }).catch(() => ({ status: { phase: 'unknown' } })); + return { ...res, id: database.id }; + }) + ); + if (workspace?.Database.length == 0) { return (
@@ -47,7 +58,7 @@ export default async function Databases({ params }: { params: { workspace: strin
))} */} - + ({ ...db, status: status.find((st) => st.id == db.id) }))} /> ); diff --git a/components/deploy/database/provider/postgres.tsx b/components/deploy/database/provider/postgres.tsx new file mode 100644 index 0000000..f5f36eb --- /dev/null +++ b/components/deploy/database/provider/postgres.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { IDatabaseConfiguration } from '@/lib/deploy/database-config'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { IDatabaseSpecs } from '@/lib/deploy/database-config'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { cn } from '@/lib/utils'; +import { Slider } from '@/components/ui/slider'; + +let specsTiers: IDatabaseSpecs[] = [ + { + id: 'postgres-xs', + cpu: 0.5, + memory: 512, + maxConnections: 5, + }, + { + id: 'postgres-s', + cpu: 1, + memory: 1024, + maxConnections: 20, + }, + { + id: 'postgres-m', + cpu: 2, + memory: 2048, + maxConnections: 50, + }, + { + id: 'postgres-l', + cpu: 4, + memory: 4096, + maxConnections: 100, + }, + { + id: 'postgres-xl', + cpu: 8, + memory: 8192, + maxConnections: 200, + }, +]; + +let MIN_STORAGE = 10; +let MAX_STORAGE = 100; + +export function PostgresSpecsPage({ config, setConfig }: { config: IDatabaseConfiguration; setConfig: Dispatch> }) { + const [specsTier, setSpecsTier] = useState(config.specs.cpu ? specsTiers.filter((tier) => tier.id == config.specs.id)[0] : specsTiers[0]); + const [storage, setStorage] = useState(config.specs.storage || MIN_STORAGE); + + useEffect(() => { + setConfig((config) => ({ + ...config, + specs: { + ...config.specs, + ...specsTier, + storage, + }, + })); + }, [specsTier, storage]); + + return ( +
+ { + setSpecsTier(specsTiers.filter((tier) => tier.id == id)[0]); + }} + > + {specsTiers.map((specs) => ( +
+ + +
+ ))} +
+
+
+ +

{storage} GB

+
+ { + setStorage(e[0] * 10); + }} + defaultValue={[storage / 10]} + /> +
+
+ ); +} diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx new file mode 100644 index 0000000..b3b620c --- /dev/null +++ b/components/ui/slider.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; + +import { cn } from '@/lib/utils'; + +const Slider = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/lib/deploy/database-config.ts b/lib/deploy/database-config.ts new file mode 100644 index 0000000..5279812 --- /dev/null +++ b/lib/deploy/database-config.ts @@ -0,0 +1,80 @@ +import { generateName } from "@/utils/name-generator"; +import { generatePassword } from "@/utils/password-generator"; + +export interface IDatabaseProvider { + id: string; + display: string; + image: { + alt: string; + src: string; + }; + versions?: IDatabaseProviderVersion[]; +} + +export interface IDatabaseProviderVersion { + id: string; + display: string; +} + +export interface IDatabaseSpecs { + id: string; + cpu: number; + memory?: number; + storage?: number; + maxConnections?: number; +} + +export interface IDatabaseConfiguration { + workspaceId: string; + name: string; + provider: IDatabaseProvider; + user: { + username: string; + password: string; + }; + specs: IDatabaseSpecs; +} + +export function defaultDatabaseConfiguration(workspaceId: string): IDatabaseConfiguration { + return { + workspaceId, + name: generateName().toLowerCase(), + provider: databaseProviders[0], + user: { + username: "deployadmin", + password: generatePassword({ length: 20, numberOfDigits: 4, numberOfSpecialCharacters: 4 }), + }, + specs: { + cpu: 0, + memory: 0, + storage: 0, + }, + }; +} + +export const databaseProviders: IDatabaseProvider[] = [ + { + id: 'vitess', + display: 'Vitess', + image: { + alt: 'Vitess', + src: '/vitess.png', + }, + }, + { + id: 'redis', + display: 'Redis', + image: { + alt: 'Redis', + src: '/redis.svg', + }, + }, + { + id: 'postgres', + display: 'Postgres', + image: { + alt: 'Postgres', + src: '/postgres.png', + }, + }, +]; \ No newline at end of file diff --git a/lib/deploy/database.ts b/lib/deploy/database.ts index 52637c4..fa9fba0 100644 --- a/lib/deploy/database.ts +++ b/lib/deploy/database.ts @@ -1,12 +1,11 @@ "use server"; -import { IDatabaseConfig } from "@/app/(deploy)/[workspace]/databases/database-new-form"; +import { IDatabaseConfiguration } from "@/lib/deploy/database-config"; import prisma from "../prisma"; import { DatabaseProvider } from "@prisma/client"; -export async function deployDatabase(config: IDatabaseConfig) { +export async function deployDatabase(config: IDatabaseConfiguration) { - // TODO: Refactor using transactions const database = await prisma.database.create({ data: { name: config.name, @@ -35,11 +34,11 @@ export async function deployDatabase(config: IDatabaseConfig) { username: config.user.username, password: config.user.password, }, + specs: config.specs, }), }); const json = await res.json(); - console.log(json) if (!res.ok) { throw new Error(json.message); @@ -62,4 +61,65 @@ export async function deployDatabase(config: IDatabaseConfig) { }) throw new Error("Failed to deploy database"); } +} + +export interface IDatabaseDeleteConfig { + id: string; + workspaceId: string; +} + +export async function deleteDatabase(config: IDatabaseDeleteConfig) { + const database = await prisma.database.findUnique({ + where: { + id: config.id + } + }); + + if (!database) { + throw new Error("Database not found"); + } + + try { + const res = await fetch(`http://127.0.0.1:8080/${config.workspaceId}/databases/${config.id}`, { + method: "DELETE", + }); + + const json = await res.json(); + + if (!res.ok && !(res.status === 404)) { + throw new Error(json.error); + } + + await prisma.database.delete({ + where: { + id: config.id, + } + }); + + return json; + } catch(err) { + throw err; + } +} + +export interface IDatabaseGetConfig { + id: string; + workspaceId: string; +} +export async function getDatabase(config: IDatabaseGetConfig) { + try { + const res = await fetch(`http://127.0.0.1:8080/${config.workspaceId}/databases/${config.id}`, { + method: "GET", + }); + + const json = await res.json(); + + if (!res.ok) { + throw new Error(json.error); + } + + return json; + } catch(err) { + throw err; + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 61be1ec..2e056aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-table": "^8.16.0", "class-variance-authority": "^0.7.0", @@ -1279,6 +1280,39 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.2.tgz", + "integrity": "sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", diff --git a/package.json b/package.json index 283b607..32c87d6 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-table": "^8.16.0", "class-variance-authority": "^0.7.0", diff --git a/utils/name-generator.ts b/utils/name-generator.ts new file mode 100644 index 0000000..fffcacc --- /dev/null +++ b/utils/name-generator.ts @@ -0,0 +1,27 @@ +var nameList = [ + 'Time', 'Past', 'Future', 'Dev', + 'Fly', 'Flying', 'Soar', 'Soaring', 'Power', 'Falling', + 'Fall', 'Jump', 'Cliff', 'Mountain', 'Rend', 'Red', 'Blue', + 'Green', 'Yellow', 'Gold', 'Demon', 'Demonic', 'Panda', 'Cat', + 'Kitty', 'Kitten', 'Zero', 'Memory', 'Trooper', 'XX', 'Bandit', + 'Fear', 'Light', 'Glow', 'Tread', 'Deep', 'Deeper', 'Deepest', + 'Mine', 'Your', 'Worst', 'Enemy', 'Hostile', 'Force', 'Video', + 'Game', 'Donkey', 'Mule', 'Colt', 'Cult', 'Cultist', 'Magnum', + 'Gun', 'Assault', 'Recon', 'Trap', 'Trapper', 'Redeem', 'Code', + 'Script', 'Writer', 'Near', 'Close', 'Open', 'Cube', 'Circle', + 'Geo', 'Genome', 'Germ', 'Spaz', 'Shot', 'Echo', 'Beta', 'Alpha', + 'Gamma', 'Omega', 'Seal', 'Squid', 'Money', 'Cash', 'Lord', 'King', + 'Duke', 'Rest', 'Fire', 'Flame', 'Morrow', 'Break', 'Breaker', 'Numb', + 'Ice', 'Cold', 'Rotten', 'Sick', 'Sickly', 'Janitor', 'Camel', 'Rooster', + 'Sand', 'Desert', 'Dessert', 'Hurdle', 'Racer', 'Eraser', 'Erase', 'Big', + 'Small', 'Short', 'Tall', 'Sith', 'Bounty', 'Hunter', 'Cracked', 'Broken', + 'Sad', 'Happy', 'Joy', 'Joyful', 'Crimson', 'Destiny', 'Deceit', 'Lies', + 'Lie', 'Honest', 'Destined', 'Bloxxer', 'Hawk', 'Eagle', 'Hawker', 'Walker', + 'Zombie', 'Sarge', 'Capt', 'Captain', 'Punch', 'One', 'Two', 'Uno', 'Slice', + 'Slash', 'Melt', 'Melted', 'Melting', 'Fell', 'Wolf', 'Hound', + 'Legacy', 'Sharp', 'Dead', 'Mew', 'Chuckle', 'Bubba', 'Bubble', 'Sandwich', 'Smasher', 'Extreme', 'Multi', 'Universe', 'Ultimate', 'Death', 'Ready', 'Monkey', 'Elevator', 'Wrench', 'Grease', 'Head', 'Theme', 'Grand', 'Cool', 'Kid', 'Boy', 'Girl', 'Vortex', 'Paradox' +]; + +export function generateName(): string { + return nameList[Math.floor(Math.random() * nameList.length)] + "-" + nameList[Math.floor(Math.random() * nameList.length)]; +} \ No newline at end of file diff --git a/utils/password-generator.ts b/utils/password-generator.ts new file mode 100644 index 0000000..061094f --- /dev/null +++ b/utils/password-generator.ts @@ -0,0 +1,33 @@ +let special = "!@$&*()_+"; +let digits = "0123456789"; +let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +interface PasswordGeneratorConfiguration { + length?: number; + numberOfDigits?: number; + numberOfSpecialCharacters?: number; +} + +export function generatePassword({ length = 16, numberOfDigits = 2, numberOfSpecialCharacters = 4 }: PasswordGeneratorConfiguration): string { + let password = ""; + + for (let i = 0; i < length - numberOfDigits - numberOfSpecialCharacters; i++) { + const randomIndex = Math.floor(Math.random() * length); + const randomLetter = Math.floor(Math.random() * letters.length); + password = password.slice(0, randomIndex) + letters[randomLetter] + password.slice(randomIndex); + } + + for (let i = 0; i < numberOfDigits; i++) { + const randomIndex = Math.floor(Math.random() * length); + const randomDigit = Math.floor(Math.random() * 10); + password = password.slice(0, randomIndex) + digits[randomDigit] + password.slice(randomIndex); + } + + for (let i = 0; i < numberOfSpecialCharacters; i++) { + const randomIndex = Math.floor(Math.random() * length); + const randomSpecial = Math.floor(Math.random() * special.length); + password = password.slice(0, randomIndex) + special[randomSpecial] + password.slice(randomIndex); + } + + return password; +} \ No newline at end of file