mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-10 05:29:23 -06:00
feat/added db templates and app templates
This commit is contained in:
30
prisma/migrations/20241222123831_migration/migration.sql
Normal file
30
prisma/migrations/20241222123831_migration/migration.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_App" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"appType" TEXT NOT NULL DEFAULT 'APP',
|
||||
"projectId" TEXT NOT NULL,
|
||||
"sourceType" TEXT NOT NULL DEFAULT 'GIT',
|
||||
"containerImageSource" TEXT,
|
||||
"gitUrl" TEXT,
|
||||
"gitBranch" TEXT,
|
||||
"gitUsername" TEXT,
|
||||
"gitToken" TEXT,
|
||||
"dockerfilePath" TEXT NOT NULL DEFAULT './Dockerfile',
|
||||
"replicas" INTEGER NOT NULL DEFAULT 1,
|
||||
"envVars" TEXT NOT NULL DEFAULT '',
|
||||
"memoryReservation" INTEGER,
|
||||
"memoryLimit" INTEGER,
|
||||
"cpuReservation" INTEGER,
|
||||
"cpuLimit" INTEGER,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "App_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_App" ("containerImageSource", "cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "id", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "sourceType", "updatedAt") SELECT "containerImageSource", "cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "id", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "sourceType", "updatedAt" FROM "App";
|
||||
DROP TABLE "App";
|
||||
ALTER TABLE "new_App" RENAME TO "App";
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -5,7 +5,7 @@
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
generator zod {
|
||||
@@ -126,6 +126,7 @@ model Project {
|
||||
model App {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
appType String @default("APP") // APP, POSTGRES, MYSQL, MONGO
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
sourceType String @default("GIT") // GIT, CONTAINER
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
placeholder
|
||||
15
public/template-icons/mariadb.svg
Normal file
15
public/template-icons/mariadb.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 309.88 252.72">
|
||||
<defs>
|
||||
<style>.cls-1{fill:#231f20;}</style>
|
||||
</defs>
|
||||
<title>MDB-VLogo_Black</title>
|
||||
<path class="cls-1" d="M63.62,217.6,74.9,261.16H66.39l-7.54-31.37L44.73,261.16H37.55L23.5,230l-7.72,31.19H7.45L18.61,217.6H26l15.21,33.73L56.32,217.6Z" transform="translate(-7.45 -9.1)"></path>
|
||||
<path class="cls-1" d="M107,234.44v-5.07h8.2v31.79H107V256c-2.18,3.69-6.64,5.8-12.07,5.8-11.34,0-17.68-8-17.68-17.2,0-8.87,6-16,16.47-16C99.68,228.65,104.63,230.7,107,234.44ZM86,245.06c0,5.85,3.68,10.85,10.8,10.85,6.88,0,10.62-4.88,10.62-10.67s-3.86-10.74-11-10.74C89.55,234.5,86,239.44,86,245.06Z" transform="translate(-7.45 -9.1)"></path>
|
||||
<path class="cls-1" d="M133.7,261.16h-8.2V229.37h8.2v7.12a12.6,12.6,0,0,1,11.47-7.84,14.75,14.75,0,0,1,5.12.84l-1.75,6a17.94,17.94,0,0,0-4.34-.6c-5.91,0-10.5,4.46-10.5,11Z" transform="translate(-7.45 -9.1)"></path>
|
||||
<path class="cls-1" d="M154.39,221a4.18,4.18,0,0,1,4.41-4.28,4.32,4.32,0,0,1,4.46,4.4,4.15,4.15,0,0,1-4.46,4.23A4.23,4.23,0,0,1,154.39,221Zm.36,8.39H163V252.6c0,1.44.3,2.47,1.51,2.47a9,9,0,0,0,1.57-.18l1.26,6a14.56,14.56,0,0,1-5.43,1c-3.44,0-7.12-1-7.12-8.81Z" transform="translate(-7.45 -9.1)"></path>
|
||||
<path class="cls-1" d="M199.64,234.44v-5.07h8.2v31.79h-8.2V256c-2.18,3.69-6.64,5.8-12.07,5.8-11.34,0-17.68-8-17.68-17.2,0-8.87,6-16,16.47-16C192.34,228.65,197.28,230.7,199.64,234.44Zm-21,10.62c0,5.85,3.68,10.85,10.8,10.85,6.88,0,10.62-4.88,10.62-10.67s-3.86-10.74-11-10.74C182.2,234.5,178.64,239.44,178.64,245.06Z" transform="translate(-7.45 -9.1)"></path>
|
||||
<path class="cls-1" d="M220.44,217.6h19.67c16.53,0,24.8,9.12,24.68,21.78.12,13.16-9,21.78-23.23,21.78H220.44Zm5.43,3.87v35.89h15c13.15,0,18.16-8.87,18.16-18.1,0-10.43-6.28-17.79-18.16-17.79Z" transform="translate(-7.45 -9.1)"></path>
|
||||
<path class="cls-1" d="M298.32,261.16h-25V217.6h22.5c8.63,0,16.83,1.63,16.71,11.29,0,6.81-4.22,8.68-8.69,9.41,6.34.54,10.14,4.58,10.14,11.1C314.07,259.17,305.5,261.16,298.32,261.16Zm-1.87-24.55c8.63,0,10.56-3.32,10.56-7.54,0-6.34-3.86-7.79-10.56-7.79H278.54v15.33Zm.24,3.68H278.54v17.19h18.94c5.31,0,10.92-1.75,10.92-8.44C308.4,241.31,301.94,240.29,296.69,240.29Z" transform="translate(-7.45 -9.1)"></path>
|
||||
<path class="cls-1" d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z" transform="translate(-7.45 -9.1)"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
3
public/template-icons/mongodb.svg
Normal file
3
public/template-icons/mongodb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="120" height="258" viewBox="0 0 120 258" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M83.0089 28.7559C72.1328 15.9086 62.7673 2.86053 60.8539 0.150554C60.6525 -0.0501848 60.3503 -0.0501848 60.1489 0.150554C58.2355 2.86053 48.8699 15.9086 37.9938 28.7559C-55.3594 147.292 52.6968 227.287 52.6968 227.287L53.6031 227.889C54.4087 240.235 56.4228 258 56.4228 258H60.451H64.4792C64.4792 258 66.4934 240.335 67.299 227.889L68.2052 227.187C68.306 227.187 176.362 147.292 83.0089 28.7559ZM60.451 225.48C60.451 225.48 55.6172 221.365 54.3081 219.257V219.057L60.1489 89.9813C60.1489 89.5798 60.7532 89.5798 60.7532 89.9813L66.594 219.057V219.257C65.2848 221.365 60.451 225.48 60.451 225.48Z" fill="#00684A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 730 B |
31
public/template-icons/mysql.svg
Normal file
31
public/template-icons/mysql.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) by Marsupilami -->
|
||||
<svg
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="svg3464"
|
||||
width="1024"
|
||||
height="710"
|
||||
viewBox="-7.928823 -7.928823 403.989606 280.151746">
|
||||
<defs
|
||||
id="defs3466" />
|
||||
<path
|
||||
style="fill:#00758f;fill-opacity:1"
|
||||
d="m 158.27673,174.58216 c -6.09836,33.17749 -14.14234,57.28763 -24.12688,72.32809 -7.7795,11.58931 -16.30187,17.38385 -25.58211,17.38385 -2.4739,0 -5.52458,-0.74716 -9.14466,-2.23175 l 0,-7.9978 c 1.76846,0.25963 3.84367,0.39813 6.23026,0.39813 4.32944,0 7.81854,-1.19972 10.47308,-3.59429 3.17888,-2.91777 4.7697,-6.1973 4.7697,-9.83165 0,-2.48235 -1.24087,-7.57605 -3.71131,-15.28134 l -16.43538,-51.17324 14.71197,0 11.79595,38.27281 c 2.64991,8.68961 3.7552,14.75582 3.31309,18.21346 6.45154,-17.278 10.95861,-36.1073 13.52283,-56.48627 l 14.183,0 m -64.96079,66.19054 -14.97552,0 C 77.8133,215.42811 76.35344,191.5959 73.96732,169.27145 l -0.1319,0 -22.80123,71.50125 -11.40235,0 -22.6631,-71.50125 -0.13351,0 c -1.68161,21.43972 -2.73954,45.27193 -3.18073,71.50125 l -13.6545,0 c 0.88238,-31.89902 3.09064,-61.80161 6.62479,-89.71471 l 18.56141,0 21.60563,65.91956 0.13258,0 21.73821,-65.91956 17.76011,0 c 3.88895,32.69112 6.18545,62.60297 6.89089,89.71471"
|
||||
id="path3414" />
|
||||
<path
|
||||
style="fill:#f29111;fill-opacity:1"
|
||||
d="m 357.6209,240.7727 -42.54601,0 0,-89.71471 14.31675,0 0,78.67871 28.22926,0 0,11.03577 M 250.64954,222.82559 c -3.62423,-5.84804 -5.43981,-15.23246 -5.43981,-28.17249 0,-22.59058 6.85116,-33.89316 20.54654,-33.89316 7.16068,0 12.4157,2.7033 15.7766,8.10434 3.61961,5.85082 5.43519,15.15395 5.43519,27.9131 0,22.77146 -6.85116,34.16182 -20.54654,34.16182 -7.15837,0 -12.4157,-2.70052 -15.77198,-8.11268 m 53.28472,20.11008 -16.44647,-8.13167 c 1.46449,-1.20412 2.85503,-2.5032 4.11392,-4.00701 6.98282,-8.23103 10.4777,-20.41371 10.4777,-36.54272 0,-29.68025 -11.6211,-44.52848 -34.86098,-44.52848 -11.39934,0 -20.28321,3.76359 -26.64466,11.30165 -6.98513,8.24099 -10.47308,20.37782 -10.47308,36.41534 0,15.76979 3.09064,27.33872 9.27655,34.68756 5.63615,6.64245 14.15967,9.9683 25.57056,9.9683 4.25715,0 8.16318,-0.52551 11.7135,-1.57677 l 21.41737,12.49674 5.83942,-10.08387 m -80.04026,-27.03045 c 0,7.60847 -2.78805,13.85464 -8.34566,18.74175 -5.56224,4.86557 -13.03476,7.30484 -22.39215,7.30484 -8.7522,0 -17.23415,-2.80752 -25.45045,-8.37671 l 3.84367,-7.70644 c 7.07059,3.54379 13.46901,5.31349 19.21372,5.31349 5.389,0 9.60687,-1.20249 12.65824,-3.5783 3.04445,-2.39295 4.86927,-5.73039 4.86927,-9.96344 0,-5.32693 -3.70508,-9.88029 -10.50311,-13.70016 -6.27599,-3.45324 -18.81873,-10.66289 -18.81873,-10.66289 -6.79572,-4.97049 -10.20282,-10.30367 -10.20282,-19.09101 0,-7.26755 2.54089,-13.14176 7.61573,-17.61106 5.0864,-4.47832 11.64882,-6.71748 19.68495,-6.71748 8.30639,0 15.85976,2.22272 22.6624,6.65217 l -3.4556,7.69926 c -5.82094,-2.47447 -11.56104,-3.71657 -17.22029,-3.71657 -4.59208,0 -8.13084,1.10545 -10.59781,3.32933 -2.48083,2.20465 -4.0146,5.04182 -4.0146,8.49969 0,5.31326 3.78592,9.92198 10.77567,13.81573 6.35453,3.45509 19.20449,10.80417 19.20449,10.80417 6.98975,4.96145 10.47308,10.25109 10.47308,18.96363"
|
||||
id="path3420" />
|
||||
<path
|
||||
id="path3422"
|
||||
d="m 367.83064,240.76344 2.37227,0 0,-9.12781 3.1045,0 0,-1.86442 -8.70138,0 0,1.86442 3.22461,0 0,9.12781 z m 18.05185,0 2.23601,0 0,-10.99223 -3.36321,0 -2.73723,7.49267 -2.97977,-7.49267 -3.24079,0 0,10.99223 2.11585,0 0,-8.36582 0.12013,0 3.12068,8.36582 1.61228,0 3.11605,-8.36582 0,8.36582"
|
||||
style="fill:#f29111;fill-rule:nonzero" />
|
||||
<path
|
||||
style="fill:#00758f;fill-opacity:1"
|
||||
id="path3424"
|
||||
d="m 238.64559,0.00159781 c -3.78299,-0.0564 -7.08132,1.37231999 -8.78516,5.55858999 -2.91971,7.0500802 4.32474,13.9842002 6.8125,17.5625002 1.83635,2.48976 4.21658,5.31295 5.50781,8.12696 0.75767,1.84358 0.97638,3.79829 1.73633,5.74609 1.72781,4.76876 3.34585,10.08247 5.6211,14.5293 1.18497,2.27666 2.48115,4.66318 3.99414,6.71289 0.87312,1.19969 2.38133,1.73434 2.70703,3.69141 -1.50836,2.16551 -1.62633,5.41875 -2.49024,8.13085 -3.88986,12.24962 -2.37931,27.42437 3.13672,36.419922 1.7255,2.70955 5.83979,8.6724 11.35352,6.39062 4.86464,-1.94775 3.78308,-8.12641 5.1875,-13.542962 0.32336,-1.31 0.10711,-2.16913 0.7539,-3.03515 l 0,0.21875 c 1.51296,3.03079 3.03113,5.950702 4.43555,8.996082 3.35628,5.30609 9.18738,10.83552 14.05664,14.51758 2.58709,1.95869 4.64362,5.31376 7.88672,6.50977 l 0,-0.32813 -0.21289,0 c -0.65139,-0.9727 -1.62205,-1.40861 -2.48828,-2.16406 -1.94496,-1.95241 -4.10118,-4.33919 -5.62109,-6.50586 -4.53895,-6.06783 -8.54397,-12.79193 -12.10352,-19.728512 -1.73471,-3.36824 -3.24562,-7.04992 -4.65234,-10.4082 -0.6468,-1.2947 -0.64713,-3.25167 -1.73047,-3.90039 -1.62615,2.38206 -3.99826,4.44356 -5.18555,7.36133 -2.05581,4.66662 -2.27091,10.410222 -3.0332,16.374992 -0.42964,0.11324 -0.21256,0.001 -0.42969,0.21679 -3.45561,-0.86664 -4.64484,-4.44351 -5.94531,-7.468742 -3.2431,-7.70644 -3.78845,-20.06341 -0.97266,-28.95312 0.75303,-2.27208 4.00591,-9.42592 2.70313,-11.59375 -0.65368,-2.06822 -2.81467,-3.25 -4.00196,-4.88281 -1.40213,-2.05898 -2.92349,-4.65619 -3.88672,-6.9375 -2.59632,-6.06807 -3.90122,-12.79132 -6.70312,-18.85938 -1.29816,-2.82327 -3.5745,-5.74941 -5.40625,-8.3457 -2.05812,-2.9275 -4.32536,-4.98636 -5.94922,-8.45117 -0.53592,-1.19509 -1.29326,-3.1483302 -0.43164,-4.4453202 0.21249,-0.86852 0.65137,-1.1944 1.51758,-1.41211 1.39751,-1.19274 5.40258,0.32185 6.80468,0.97266 4.00306,1.62124 7.35414,3.1459202 10.70118,5.4179702 1.51527,1.0862 3.13745,3.14677 5.08008,3.69336 l 2.27539,0 c 3.45791,0.75272 7.35225,0.20976 10.59765,1.18945 5.72624,1.84126 10.91427,4.55102 15.56641,7.47852 14.15736,8.99787 25.8324,21.79288 33.7207,37.07422 1.30047,2.48975 1.84592,4.76698 3.0332,7.36328 2.27063,5.32229 5.08556,10.74348 7.35157,15.9414 2.26834,5.09532 4.43392,10.29337 7.68164,14.52735 1.61693,2.272502 8.10568,3.464962 11.02539,4.658192 2.15747,0.97251 5.51369,1.84057 7.46094,3.03125 3.67504,2.27252 7.34374,4.87783 10.80859,7.375 1.72547,1.30093 7.13134,4.00839 7.45703,6.17969 l 0.002,0.002 c -8.65287,-0.217 -15.35462,0.64846 -20.97461,3.03516 -1.62154,0.64965 -4.21379,0.64908 -4.43554,2.70898 0.87083,0.86574 0.97604,2.27503 1.73828,3.47266 1.29354,2.16783 3.55907,5.08968 5.61719,6.61133 2.2706,1.73519 4.54037,3.46322 6.92187,4.98047 4.21325,2.61274 8.97184,4.12378 13.07422,6.72656 2.38612,1.51539 4.75645,3.46543 7.14258,5.0957 1.18498,0.86667 1.93516,2.27806 3.45508,2.81445 l 0,-0.33007 c -0.76229,-0.97275 -0.97354,-2.38054 -1.72657,-3.47071 -1.07872,-1.07557 -2.16275,-2.05743 -3.24609,-3.13672 -3.13684,-4.2268 -7.03391,-7.90989 -11.24023,-10.94531 -3.46254,-2.39109 -11.0338,-5.64391 -12.4336,-9.64258 0,0 -0.11445,-0.11393 -0.2207,-0.2207 2.37919,-0.22003 5.19652,-1.08856 7.46484,-1.74609 3.67043,-0.97275 7.02108,-0.75382 10.80469,-1.72656 1.73242,-0.43681 3.46218,-0.98075 5.19922,-1.51368 l 0,-0.98242 c -1.95648,-1.94685 -3.35471,-4.5535 -5.4082,-6.39453 -5.50911,-4.76783 -11.57079,-9.42564 -17.83985,-13.33398 -3.35628,-2.16853 -7.68121,-3.570682 -11.25,-5.417972 -1.29121,-0.6515 -3.45098,-0.97233 -4.21094,-2.0625 -1.94726,-2.38391 -3.03343,-5.52289 -4.43554,-8.3457 -3.13222,-5.95527 -6.16337,-12.56593 -8.86133,-18.86328 -1.94491,-4.22448 -3.13448,-8.44865 -5.51367,-12.35352 -11.14064,-18.43116 -23.24517,-29.5907 -41.83985,-40.54101 -4.00074,-2.279 -8.75683,-3.25255 -13.83398,-4.44531 -2.7072,-0.11349 -5.4068,-0.32511 -8.10938,-0.43164 -1.73247,-0.7620402 -3.46594,-2.8238202 -4.97661,-3.7988802 -3.8532,-2.43765 -11.50361,-6.65404999 -17.80859,-6.74804999 z M 265.31746,26.796518 c -1.83403,0 -3.1285,0.22297 -4.43359,0.54492 l 0,0.2168 0.21094,0 c 0.87312,1.73009 2.38914,2.93257 3.46093,4.44727 0.87084,1.73477 1.62827,3.46217 2.49219,5.19922 0.10623,-0.10652 0.20899,-0.21875 0.20899,-0.21875 1.52451,-1.07926 2.28125,-2.81241 2.28125,-5.41797 -0.6537,-0.76432 -0.75567,-1.51732 -1.30078,-2.2793 -0.64678,-1.08157 -2.05372,-1.62367 -2.91993,-2.49219 z" />
|
||||
</svg>
|
||||
<!-- version: 20171223, original size: 388.13196 264.2941, border: 3% -->
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
22
public/template-icons/postgres.svg
Normal file
22
public/template-icons/postgres.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg width="432.071pt" height="445.383pt" viewBox="0 0 432.071 445.383" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="orginal" style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
|
||||
</g>
|
||||
<g id="Layer_x0020_3" style="fill-rule:nonzero;clip-rule:nonzero;fill:none;stroke:#FFFFFF;stroke-width:12.4651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;">
|
||||
<path style="fill:#000000;stroke:#000000;stroke-width:37.3953;stroke-linecap:butt;stroke-linejoin:miter;" d="M323.205,324.227c2.833-23.601,1.984-27.062,19.563-23.239l4.463,0.392c13.517,0.615,31.199-2.174,41.587-7c22.362-10.376,35.622-27.7,13.572-23.148c-50.297,10.376-53.755-6.655-53.755-6.655c53.111-78.803,75.313-178.836,56.149-203.322 C352.514-5.534,262.036,26.049,260.522,26.869l-0.482,0.089c-9.938-2.062-21.06-3.294-33.554-3.496c-22.761-0.374-40.032,5.967-53.133,15.904c0,0-161.408-66.498-153.899,83.628c1.597,31.936,45.777,241.655,98.47,178.31 c19.259-23.163,37.871-42.748,37.871-42.748c9.242,6.14,20.307,9.272,31.912,8.147l0.897-0.765c-0.281,2.876-0.157,5.689,0.359,9.019c-13.572,15.167-9.584,17.83-36.723,23.416c-27.457,5.659-11.326,15.734-0.797,18.367c12.768,3.193,42.305,7.716,62.268-20.224 l-0.795,3.188c5.325,4.26,4.965,30.619,5.72,49.452c0.756,18.834,2.017,36.409,5.856,46.771c3.839,10.36,8.369,37.05,44.036,29.406c29.809-6.388,52.6-15.582,54.677-101.107"/>
|
||||
<path style="fill:#336791;stroke:none;" d="M402.395,271.23c-50.302,10.376-53.76-6.655-53.76-6.655c53.111-78.808,75.313-178.843,56.153-203.326c-52.27-66.785-142.752-35.2-144.262-34.38l-0.486,0.087c-9.938-2.063-21.06-3.292-33.56-3.496c-22.761-0.373-40.026,5.967-53.127,15.902 c0,0-161.411-66.495-153.904,83.63c1.597,31.938,45.776,241.657,98.471,178.312c19.26-23.163,37.869-42.748,37.869-42.748c9.243,6.14,20.308,9.272,31.908,8.147l0.901-0.765c-0.28,2.876-0.152,5.689,0.361,9.019c-13.575,15.167-9.586,17.83-36.723,23.416 c-27.459,5.659-11.328,15.734-0.796,18.367c12.768,3.193,42.307,7.716,62.266-20.224l-0.796,3.188c5.319,4.26,9.054,27.711,8.428,48.969c-0.626,21.259-1.044,35.854,3.147,47.254c4.191,11.4,8.368,37.05,44.042,29.406c29.809-6.388,45.256-22.942,47.405-50.555 c1.525-19.631,4.976-16.729,5.194-34.28l2.768-8.309c3.192-26.611,0.507-35.196,18.872-31.203l4.463,0.392c13.517,0.615,31.208-2.174,41.591-7c22.358-10.376,35.618-27.7,13.573-23.148z"/>
|
||||
<path d="M215.866,286.484c-1.385,49.516,0.348,99.377,5.193,111.495c4.848,12.118,15.223,35.688,50.9,28.045c29.806-6.39,40.651-18.756,45.357-46.051c3.466-20.082,10.148-75.854,11.005-87.281"/>
|
||||
<path d="M173.104,38.256c0,0-161.521-66.016-154.012,84.109c1.597,31.938,45.779,241.664,98.473,178.316c19.256-23.166,36.671-41.335,36.671-41.335"/>
|
||||
<path d="M260.349,26.207c-5.591,1.753,89.848-34.889,144.087,34.417c19.159,24.484-3.043,124.519-56.153,203.329"/>
|
||||
<path style="stroke-linejoin:bevel;" d="M348.282,263.953c0,0,3.461,17.036,53.764,6.653c22.04-4.552,8.776,12.774-13.577,23.155c-18.345,8.514-59.474,10.696-60.146-1.069c-1.729-30.355,21.647-21.133,19.96-28.739c-1.525-6.85-11.979-13.573-18.894-30.338 c-6.037-14.633-82.796-126.849,21.287-110.183c3.813-0.789-27.146-99.002-124.553-100.599c-97.385-1.597-94.19,119.762-94.19,119.762"/>
|
||||
<path d="M188.604,274.334c-13.577,15.166-9.584,17.829-36.723,23.417c-27.459,5.66-11.326,15.733-0.797,18.365c12.768,3.195,42.307,7.718,62.266-20.229c6.078-8.509-0.036-22.086-8.385-25.547c-4.034-1.671-9.428-3.765-16.361,3.994z"/>
|
||||
<path d="M187.715,274.069c-1.368-8.917,2.93-19.528,7.536-31.942c6.922-18.626,22.893-37.255,10.117-96.339c-9.523-44.029-73.396-9.163-73.436-3.193c-0.039,5.968,2.889,30.26-1.067,58.548c-5.162,36.913,23.488,68.132,56.479,64.938"/>
|
||||
<path style="fill:#FFFFFF;stroke-width:4.155;stroke-linecap:butt;stroke-linejoin:miter;" d="M172.517,141.7c-0.288,2.039,3.733,7.48,8.976,8.207c5.234,0.73,9.714-3.522,9.998-5.559c0.284-2.039-3.732-4.285-8.977-5.015c-5.237-0.731-9.719,0.333-9.996,2.367z"/>
|
||||
<path style="fill:#FFFFFF;stroke-width:2.0775;stroke-linecap:butt;stroke-linejoin:miter;" d="M331.941,137.543c0.284,2.039-3.732,7.48-8.976,8.207c-5.238,0.73-9.718-3.522-10.005-5.559c-0.277-2.039,3.74-4.285,8.979-5.015c5.239-0.73,9.718,0.333,10.002,2.368z"/>
|
||||
<path d="M350.676,123.432c0.863,15.994-3.445,26.888-3.988,43.914c-0.804,24.748,11.799,53.074-7.191,81.435"/>
|
||||
<path style="stroke-width:3;" d="M0,60.232"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/template-icons/wordpress.png
Normal file
BIN
public/template-icons/wordpress.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
129
src/__tests__/server/utils/app-template.utils.test.ts
Normal file
129
src/__tests__/server/utils/app-template.utils.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { AppTemplateUtils } from '@/server/utils/app-template.utils';
|
||||
import appTemplateService from '../../../server/services/app-template.service';
|
||||
import { AppTemplateContentModel, AppTemplateInputSettingsModel } from '@/shared/model/app-template.model';
|
||||
import crypto from 'crypto';
|
||||
import { AppExtendedModel } from '@/shared/model/app-extended.model';
|
||||
import { DatabaseTemplateInfoModel } from '@/shared/model/database-template-info.model';
|
||||
import { KubeObjectNameUtils } from '@/server/utils/kube-object-name.utils';
|
||||
import { ServiceException } from '@/shared/model/service.exception.model';
|
||||
|
||||
jest.mock('crypto', () => ({
|
||||
randomBytes: jest.fn(() => ({
|
||||
toString: jest.fn(() => 'mockedRandomValue')
|
||||
}))
|
||||
}));
|
||||
|
||||
describe('AppTemplateService', () => {
|
||||
describe('populateRandomValues', () => {
|
||||
it('should populate random values for inputs with randomGeneratedIfEmpty set to true and value is empty', () => {
|
||||
const inputValues: AppTemplateInputSettingsModel[] = [
|
||||
{ key: 'key1', value: '', randomGeneratedIfEmpty: true, isEnvVar: false, label: '' },
|
||||
{ key: 'key2', value: 'existingValue', randomGeneratedIfEmpty: true, isEnvVar: false, label: '' },
|
||||
{ key: 'key3', value: '', randomGeneratedIfEmpty: false, isEnvVar: false, label: '' }
|
||||
];
|
||||
|
||||
AppTemplateUtils.populateRandomValues(inputValues);
|
||||
|
||||
expect(inputValues[0].value).toBe('mockedRandomValue');
|
||||
expect(inputValues[1].value).toBe('existingValue');
|
||||
expect(inputValues[2].value).toBe('');
|
||||
});
|
||||
|
||||
it('should not change values for inputs with randomGeneratedIfEmpty set to false', () => {
|
||||
const inputValues: AppTemplateInputSettingsModel[] = [
|
||||
{ key: 'key1', value: '', randomGeneratedIfEmpty: false, isEnvVar: false, label: '' }
|
||||
];
|
||||
|
||||
AppTemplateUtils.populateRandomValues(inputValues);
|
||||
|
||||
expect(inputValues[0].value).toBe('');
|
||||
});
|
||||
|
||||
it('should not change values for inputs with randomGeneratedIfEmpty set to true but value is not empty', () => {
|
||||
const inputValues: AppTemplateInputSettingsModel[] = [
|
||||
{ key: 'key1', value: 'existingValue', randomGeneratedIfEmpty: true, isEnvVar: false, label: '' }
|
||||
];
|
||||
|
||||
AppTemplateUtils.populateRandomValues(inputValues);
|
||||
|
||||
expect(inputValues[0].value).toBe('existingValue');
|
||||
});
|
||||
|
||||
jest.mock('crypto', () => ({
|
||||
randomBytes: jest.fn(() => ({
|
||||
toString: jest.fn(() => 'mockedRandomValue')
|
||||
}))
|
||||
}));
|
||||
|
||||
|
||||
|
||||
describe('mapTemplateInputValuesToApp', () => {
|
||||
it('should map template input values to app model', () => {
|
||||
const appTemplate: any = {
|
||||
appModel: { envVars: '' }
|
||||
};
|
||||
const inputValues: AppTemplateInputSettingsModel[] = [
|
||||
{ key: 'ENV_VAR1', value: 'value1', randomGeneratedIfEmpty: false, isEnvVar: true, label: '' },
|
||||
{ key: 'configKey', value: 'configValue', randomGeneratedIfEmpty: false, isEnvVar: false, label: '' }
|
||||
];
|
||||
|
||||
const app = AppTemplateUtils.mapTemplateInputValuesToApp(appTemplate, inputValues);
|
||||
|
||||
expect(app.envVars).toContain('ENV_VAR1=value1');
|
||||
expect((app as any).configKey).toBe('configValue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('replacePlaceholdersInEnvVariablesWithDatabaseInfo', () => {
|
||||
it('should replace placeholders in env variables with database info', () => {
|
||||
const app: AppExtendedModel = {
|
||||
envVars: 'DB_NAME={databaseName} {username} {password} {port} {hostname}',
|
||||
appType: 'MYSQL',
|
||||
appPorts: [{ port: 3306 }],
|
||||
id: 'app-id'
|
||||
} as AppExtendedModel;
|
||||
const databaseInfo: DatabaseTemplateInfoModel = {
|
||||
databaseName: 'testDB',
|
||||
username: 'testUser',
|
||||
password: 'testPass',
|
||||
port: 3306,
|
||||
hostname: 'localhost'
|
||||
};
|
||||
|
||||
AppTemplateUtils.replacePlaceholdersInEnvVariablesWithDatabaseInfo(app, databaseInfo);
|
||||
|
||||
expect(app.envVars).toBe('DB_NAME=testDB testUser testPass 3306 localhost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDatabaseModelFromApp', () => {
|
||||
it('should return database model for MONGODB app type', () => {
|
||||
const app: AppExtendedModel = {
|
||||
appType: 'MONGODB',
|
||||
envVars: 'MONGO_INITDB_DATABASE=testDB\nMONGO_INITDB_ROOT_USERNAME=testUser\nMONGO_INITDB_ROOT_PASSWORD=testPass\n',
|
||||
appPorts: [{ port: 27017 }],
|
||||
id: 'app-id'
|
||||
} as AppExtendedModel;
|
||||
|
||||
const databaseModel = AppTemplateUtils.getDatabaseModelFromApp(app);
|
||||
|
||||
expect(databaseModel.databaseName).toBe('testDB');
|
||||
expect(databaseModel.username).toBe('testUser');
|
||||
expect(databaseModel.password).toBe('testPass');
|
||||
expect(databaseModel.port).toBe(27017);
|
||||
expect(databaseModel.hostname).toBe(KubeObjectNameUtils.toServiceName('app-id'));
|
||||
});
|
||||
|
||||
it('should throw ServiceException for unknown app type', () => {
|
||||
const app: AppExtendedModel = {
|
||||
appType: 'UNKNOWN',
|
||||
envVars: '',
|
||||
appPorts: [],
|
||||
id: 'app-id'
|
||||
} as any;
|
||||
|
||||
expect(() => AppTemplateUtils.getDatabaseModelFromApp(app)).toThrow(ServiceException);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -40,7 +40,7 @@ export default function DefaultPortEditDialog({ children, appPort, appId }: { ch
|
||||
if (state.status === 'success') {
|
||||
form.reset();
|
||||
toast.success('Port saved successfully. ', {
|
||||
description: "Klick \"deploy\" to apply the changes to your app.",
|
||||
description: "Click \"deploy\" to apply the changes to your app.",
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function DialogEditDialog({ children, domain, appId }: { children
|
||||
if (state.status === 'success') {
|
||||
form.reset();
|
||||
toast.success('Domain saved successfully. ', {
|
||||
description: "Klick \"deploy\" to apply the changes to your app.",
|
||||
description: "Click \"deploy\" to apply the changes to your app.",
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ export default function EnvEdit({ app }: {
|
||||
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: AppEnvVariablesModel) => saveEnvVariables(state, payload, app.id), FormUtils.getInitialFormState<typeof appEnvVariablesZodModel>());
|
||||
useEffect(() => {
|
||||
if (state.status === 'success') {
|
||||
toast.success('Env Variables Limits Saved');
|
||||
toast.success('Env Variables Limits Saved', {
|
||||
description: "Click \"deploy\" to apply the changes to your app.",
|
||||
});
|
||||
}
|
||||
FormUtils.mapValidationErrorsToForm<typeof appEnvVariablesZodModel>(state, form);
|
||||
}, [state]);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { App } from "@prisma/client";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import { cn } from "@/frontend/utils/utils";
|
||||
|
||||
|
||||
export default function GeneralAppRateLimits({ app }: {
|
||||
@@ -31,7 +32,9 @@ export default function GeneralAppRateLimits({ app }: {
|
||||
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: AppRateLimitsModel) => saveGeneralAppRateLimits(state, payload, app.id), FormUtils.getInitialFormState<typeof appRateLimitsZodModel>());
|
||||
useEffect(() => {
|
||||
if (state.status === 'success') {
|
||||
toast.success('Rate Limits Saved');
|
||||
toast.success('Rate Limits Saved', {
|
||||
description: "Click \"deploy\" to apply the changes to your app.",
|
||||
});
|
||||
}
|
||||
FormUtils.mapValidationErrorsToForm<typeof appRateLimitsZodModel>(state, form);
|
||||
}, [state]);
|
||||
@@ -47,7 +50,7 @@ export default function GeneralAppRateLimits({ app }: {
|
||||
return formAction(data);
|
||||
})()}>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className={cn('grid grid-cols-2 gap-4 ', app.appType !== 'APP' && 'hidden')}>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -32,7 +32,9 @@ export default function GeneralAppSource({ app }: {
|
||||
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: AppSourceInfoInputModel) => saveGeneralAppSourceInfo(state, payload, app.id), FormUtils.getInitialFormState<typeof appSourceInfoInputZodModel>());
|
||||
useEffect(() => {
|
||||
if (state.status === 'success') {
|
||||
toast.success('Source Info Saved');
|
||||
toast.success('Source Info Saved', {
|
||||
description: "Click \"deploy\" to apply the changes to your app.",
|
||||
});
|
||||
}
|
||||
FormUtils.mapValidationErrorsToForm<typeof appSourceInfoInputZodModel>(state, form)
|
||||
}, [state]);
|
||||
@@ -69,7 +71,7 @@ export default function GeneralAppSource({ app }: {
|
||||
form.setValue('sourceType', val as 'GIT' | 'CONTAINER');
|
||||
}} className="mt-2">
|
||||
<TabsList>
|
||||
<TabsTrigger value="GIT">Git</TabsTrigger>
|
||||
{app.appType === 'APP' && <TabsTrigger value="GIT">Git</TabsTrigger>}
|
||||
<TabsTrigger value="CONTAINER">Docker Container</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="GIT" className="space-y-4 mt-4">
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function DialogEditDialog({ children, volume, app }: { children:
|
||||
if (state.status === 'success') {
|
||||
form.reset();
|
||||
toast.success('Volume saved successfully', {
|
||||
description: "Klick \"deploy\" to apply the changes to your app.",
|
||||
description: "Click \"deploy\" to apply the changes to your app.",
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,75 @@
|
||||
'use client'
|
||||
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AppTemplateModel } from "@/shared/model/app-template.model"
|
||||
import { allTemplates } from "@/shared/templates/all.templates"
|
||||
import { allTemplates, appTemplates, databaseTemplates } from "@/shared/templates/all.templates"
|
||||
import CreateTemplateAppSetupDialog from "./create-template-app-setup-dialog"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
|
||||
|
||||
export default function ChooseTemplateDialog({
|
||||
projectId,
|
||||
children
|
||||
templateType,
|
||||
onClose
|
||||
}: {
|
||||
projectId: string;
|
||||
children: React.ReactNode;
|
||||
templateType: 'database' | 'template' | undefined;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [chosenAppTemplate, setChosenAppTemplate] = useState<AppTemplateModel | undefined>(undefined);
|
||||
const [displayedTemplates, setDisplayedTemplates] = useState<AppTemplateModel[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (templateType) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
if (templateType === 'database') {
|
||||
setDisplayedTemplates(databaseTemplates);
|
||||
}
|
||||
if (templateType === 'template') {
|
||||
setDisplayedTemplates(appTemplates);
|
||||
}
|
||||
}, [templateType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateTemplateAppSetupDialog appTemplate={chosenAppTemplate} projectId={projectId}
|
||||
dialogClosed={() => setChosenAppTemplate(undefined)} />
|
||||
|
||||
<div onClick={() => setIsOpen(true)}>{children}</div>
|
||||
<Dialog open={!!isOpen} onOpenChange={(isOpened) => setIsOpen(false)}>
|
||||
dialogClosed={() => {
|
||||
setChosenAppTemplate(undefined);
|
||||
onClose();
|
||||
}} />
|
||||
<Dialog open={!!isOpen} onOpenChange={(isOpened) => {
|
||||
setIsOpen(isOpened);
|
||||
if (!isOpened) {
|
||||
onClose();
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[1000px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create App from Template</DialogTitle>
|
||||
<DialogTitle>Create {templateType === 'database' ? 'Database' : 'App'} from Template</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose a Template you want to deploy.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{allTemplates.map((template) => (
|
||||
<div key={template.name}
|
||||
className="bg-white rounded-md p-4 border border-gray-200 text-center hover:bg-slate-50 active:bg-slate-100 transition-all cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setChosenAppTemplate(template);
|
||||
}} >
|
||||
<h3 className="text-lg font-semibold py-5">{template.name}</h3>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ScrollArea className="max-h-[70vh]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 px-1">
|
||||
{displayedTemplates.map((template) => (
|
||||
<div key={template.name}
|
||||
className="h-42 grid grid-cols-1 gap-4 items-center bg-white rounded-md p-4 border border-gray-200 text-center hover:bg-slate-50 active:bg-slate-100 transition-all cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setChosenAppTemplate(template);
|
||||
}} >
|
||||
{template.iconName && <img src={`/template-icons/${template.iconName}`} className="h-10 mx-auto" />}
|
||||
<h3 className="text-lg font-semibold">{template.name}</h3>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
40
src/app/project/create-project-actions.tsx
Normal file
40
src/app/project/create-project-actions.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { EditAppDialog } from "./edit-app-dialog";
|
||||
import { Blocks, Database, File, LayoutGrid, Plus } from "lucide-react";
|
||||
import ChooseTemplateDialog from "./choose-template-dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
export default function CreateProjectActions({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
|
||||
const [templateType, setTemplateType] = useState<"database" | "template" | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChooseTemplateDialog projectId={projectId} templateType={templateType} onClose={() => setTemplateType(undefined)} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild><Button><Plus /> Create App</Button></DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<EditAppDialog projectId={projectId}>
|
||||
<DropdownMenuItem><File /> Empty App</DropdownMenuItem>
|
||||
</EditAppDialog>
|
||||
<DropdownMenuItem onClick={() => setTemplateType('database')}><Database /> Database</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTemplateType('template')}><Blocks /> Template</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -20,6 +21,7 @@ import { ServerActionResult } from "@/shared/model/server-action-error-return.mo
|
||||
import { toast } from "sonner"
|
||||
import { AppTemplateModel, appTemplateZodModel } from "@/shared/model/app-template.model"
|
||||
import { createAppFromTemplate } from "./actions"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
|
||||
export default function CreateTemplateAppSetupDialog({
|
||||
appTemplate,
|
||||
@@ -45,7 +47,10 @@ export default function CreateTemplateAppSetupDialog({
|
||||
useEffect(() => {
|
||||
if (state.status === 'success') {
|
||||
form.reset();
|
||||
toast.success('App created successfully');
|
||||
const appLabel = ((appTemplate?.templates.length ?? 0) > 1) ? 'Apps' : 'App';
|
||||
toast.success(`${appLabel} Created successfully`, {
|
||||
description: `Click deploy to start the ${appLabel}.`,
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
FormUtils.mapValidationErrorsToForm<typeof appTemplateZodModel>(state, form);
|
||||
@@ -66,54 +71,65 @@ export default function CreateTemplateAppSetupDialog({
|
||||
dialogClosed();
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create App "{appTemplate?.name}"</DialogTitle>
|
||||
<DialogDescription>
|
||||
Insert your values for the template.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form action={(e) => form.handleSubmit((data) => {
|
||||
return formAction(data);
|
||||
})()}>
|
||||
{appTemplate?.templates.map((t, templateIndex) =>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`templates[${templateIndex}].appModel.name` as any}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>App Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{t.inputSettings.map((input, settingsIndex) => (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`templates[${templateIndex}].inputSettings[${settingsIndex}].value` as any}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{input.label}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<ScrollArea className="max-h-[70vh]">
|
||||
<div className="px-2">
|
||||
<Form {...form} >
|
||||
<form action={(e) => form.handleSubmit((data) => {
|
||||
return formAction(data);
|
||||
})()}>
|
||||
<div className="space-y-6">
|
||||
{appTemplate?.templates.map((t, templateIndex) => (
|
||||
<>
|
||||
{templateIndex > 0 && <div className="border-t pb-4"></div>}
|
||||
{appTemplate?.templates.length > 1 &&
|
||||
<div className="text-2xl font-semibold">{t.appModel.name}</div>}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`templates[${templateIndex}].appModel.name` as any}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>App Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{t.inputSettings.map((input, settingsIndex) => (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`templates[${templateIndex}].inputSettings[${settingsIndex}].value` as any}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{input.label}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
{input.randomGeneratedIfEmpty &&
|
||||
<FormDescription>If left empty, a random value will be generated.</FormDescription>}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
<p className="text-red-500">{state.message}</p>
|
||||
<SubmitButton>Create</SubmitButton>
|
||||
</div>
|
||||
|
||||
<p className="text-red-500">{state.message}</p>
|
||||
<SubmitButton>Save</SubmitButton>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form >
|
||||
</form>
|
||||
</Form >
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
'use server'
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import Link from "next/link";
|
||||
import { getAuthUserSession, getUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAuthUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import projectService from "@/server/services/project.service";
|
||||
import AppTable from "./apps-table";
|
||||
import { EditAppDialog } from "./edit-app-dialog";
|
||||
import appService from "@/server/services/app.service";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb"
|
||||
import PageTitle from "@/components/custom/page-title";
|
||||
import ProjectBreadcrumbs from "./project-breadcrumbs";
|
||||
import { Plus } from "lucide-react";
|
||||
import ChooseTemplateDialog from "./choose-template-dialog";
|
||||
import CreateProjectActions from "./create-project-actions";
|
||||
|
||||
|
||||
export default async function AppsPage({
|
||||
@@ -40,8 +28,7 @@ export default async function AppsPage({
|
||||
<PageTitle
|
||||
title="Apps"
|
||||
subtitle={`All Apps for Project "${project.name}"`}>
|
||||
<ChooseTemplateDialog projectId={projectId}><Button variant="secondary"><Plus /> Create App from Template</Button></ChooseTemplateDialog>
|
||||
<EditAppDialog projectId={projectId}><Button><Plus /> Create App</Button></EditAppDialog>
|
||||
<CreateProjectActions projectId={projectId} />
|
||||
</PageTitle>
|
||||
<AppTable app={data} projectId={project.id} />
|
||||
<ProjectBreadcrumbs project={project} />
|
||||
|
||||
@@ -2,6 +2,10 @@ import { AppTemplateContentModel, AppTemplateInputSettingsModel, AppTemplateMode
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
import appService from "./app.service";
|
||||
import { allTemplates } from "@/shared/templates/all.templates";
|
||||
import { AppTemplateUtils } from "../utils/app-template.utils";
|
||||
import { DatabaseTemplateInfoModel } from "@/shared/model/database-template-info.model";
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { Tags } from "../utils/cache-tag-generator.utils";
|
||||
|
||||
class AppTemplateService {
|
||||
|
||||
@@ -10,18 +14,34 @@ class AppTemplateService {
|
||||
throw new ServiceException(`Template with name '${template.name}' not found.`);
|
||||
}
|
||||
|
||||
let databaseInfo: DatabaseTemplateInfoModel | undefined;
|
||||
|
||||
for (const tmpl of template.templates) {
|
||||
await this.createAppFromTemplateContent(projectId, tmpl, tmpl.inputSettings);
|
||||
const createdAppId = await this.createAppFromTemplateContent(projectId, tmpl, tmpl.inputSettings);
|
||||
const extendedApp = await appService.getExtendedById(createdAppId, false);
|
||||
|
||||
// used for templates with multiple apps and a database
|
||||
if (databaseInfo) {
|
||||
AppTemplateUtils.replacePlaceholdersInEnvVariablesWithDatabaseInfo(extendedApp, databaseInfo);
|
||||
await appService.save({
|
||||
id: createdAppId,
|
||||
envVars: extendedApp.envVars
|
||||
});
|
||||
}
|
||||
if (extendedApp.appType !== 'APP') {
|
||||
databaseInfo = AppTemplateUtils.getDatabaseModelFromApp(extendedApp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createAppFromTemplateContent(projectId: string, template: AppTemplateContentModel, inputValues: AppTemplateInputSettingsModel[]) {
|
||||
private async createAppFromTemplateContent(projectId: string, template: AppTemplateContentModel,
|
||||
inputValues: AppTemplateInputSettingsModel[]) {
|
||||
|
||||
const mappedApp = this.mapTemplateInputValuesToApp(template, inputValues);
|
||||
const mappedApp = AppTemplateUtils.mapTemplateInputValuesToApp(template, inputValues);
|
||||
const createdApp = await appService.save({
|
||||
...mappedApp,
|
||||
projectId
|
||||
});
|
||||
}, false);
|
||||
|
||||
const savedDomains = await Promise.all(template.appDomains.map(async x => {
|
||||
return await appService.saveDomain({
|
||||
@@ -46,25 +66,6 @@ class AppTemplateService {
|
||||
|
||||
return createdApp.id;
|
||||
}
|
||||
|
||||
mapTemplateInputValuesToApp(appTemplate: AppTemplateContentModel,
|
||||
inputValues: AppTemplateInputSettingsModel[]) {
|
||||
|
||||
const app = { ...appTemplate.appModel };
|
||||
|
||||
const envVariables = inputValues.filter(x => x.isEnvVar);
|
||||
const otherConfigValues = inputValues.filter(x => !x.isEnvVar);
|
||||
|
||||
for (const envVariable of envVariables) {
|
||||
app.envVars += `${envVariable.key}=${envVariable.value}\n`;
|
||||
}
|
||||
|
||||
for (const configValue of otherConfigValues) {
|
||||
(app as any)[configValue.key] = configValue.value;
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
const appTermplateService = new AppTemplateService();
|
||||
|
||||
@@ -81,20 +81,30 @@ class AppService {
|
||||
})(projectId as string);
|
||||
}
|
||||
|
||||
async getExtendedById(appId: string): Promise<AppExtendedModel> {
|
||||
return await unstable_cache(async (id: string) => await dataAccess.client.app.findFirstOrThrow({
|
||||
where: {
|
||||
id
|
||||
}, include: {
|
||||
project: true,
|
||||
appDomains: true,
|
||||
appVolumes: true,
|
||||
appPorts: true
|
||||
}
|
||||
}),
|
||||
[Tags.app(appId)], {
|
||||
tags: [Tags.app(appId)]
|
||||
})(appId);
|
||||
async getExtendedById(appId: string, cached = true): Promise<AppExtendedModel> {
|
||||
const include = {
|
||||
project: true,
|
||||
appDomains: true,
|
||||
appVolumes: true,
|
||||
appPorts: true
|
||||
};
|
||||
if (cached) {
|
||||
return await unstable_cache(async (id: string) => await dataAccess.client.app.findFirstOrThrow({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
include
|
||||
}),
|
||||
[Tags.app(appId)], {
|
||||
tags: [Tags.app(appId)]
|
||||
})(appId);
|
||||
} else {
|
||||
return await dataAccess.client.app.findFirstOrThrow({
|
||||
where: {
|
||||
id: appId
|
||||
}, include
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getById(appId: string) {
|
||||
@@ -108,7 +118,7 @@ class AppService {
|
||||
})(appId);
|
||||
}
|
||||
|
||||
async save(item: Prisma.AppUncheckedCreateInput | Prisma.AppUncheckedUpdateInput) {
|
||||
async save(item: Prisma.AppUncheckedCreateInput | Prisma.AppUncheckedUpdateInput, createDefaultPort = true) {
|
||||
let savedItem: App;
|
||||
try {
|
||||
if (item.id) {
|
||||
@@ -123,13 +133,15 @@ class AppService {
|
||||
savedItem = await dataAccess.client.app.create({
|
||||
data: item as Prisma.AppUncheckedCreateInput
|
||||
});
|
||||
// add default port 80
|
||||
await dataAccess.client.appPort.create({
|
||||
data: {
|
||||
appId: savedItem.id,
|
||||
port: 80
|
||||
}
|
||||
});
|
||||
if (createDefaultPort) {
|
||||
// add default port 80
|
||||
await dataAccess.client.appPort.create({
|
||||
data: {
|
||||
appId: savedItem.id,
|
||||
port: 80
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
revalidateTag(Tags.apps(item.projectId as string));
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Constants } from "../../shared/utils/constants";
|
||||
import svcService from "./svc.service";
|
||||
import { dlog } from "./deployment-logs.service";
|
||||
import registryService from "./registry.service";
|
||||
import { EnvVarUtils } from "../utils/env-var.utils";
|
||||
|
||||
class DeploymentService {
|
||||
|
||||
@@ -57,7 +58,7 @@ class DeploymentService {
|
||||
dlog(deploymentId, `Configured ${volumes.length} Storage Volumes.`);
|
||||
}
|
||||
|
||||
const envVars = this.parseEnvVariables(app);
|
||||
const envVars = EnvVarUtils.parseEnvVariables(app);
|
||||
dlog(deploymentId, `Configured ${envVars.length} Env Variables.`);
|
||||
|
||||
const existingDeployment = await this.getDeployment(app.projectId, app.id);
|
||||
@@ -160,14 +161,6 @@ class DeploymentService {
|
||||
dlog(deploymentId, `Deployment applied`);
|
||||
}
|
||||
|
||||
private parseEnvVariables(app: AppExtendedModel) {
|
||||
return app.envVars ? app.envVars.split('\n').filter(x => !!x).map(env => {
|
||||
const [name] = env.split('=');
|
||||
const value = env.replace(`${name}=`, '');
|
||||
return { name, value };
|
||||
}) : [];
|
||||
}
|
||||
|
||||
async setReplicasForDeployment(projectId: string, appId: string, replicas: number) {
|
||||
const existingDeployment = await this.getDeployment(projectId, appId);
|
||||
if (!existingDeployment) {
|
||||
|
||||
111
src/server/utils/app-template.utils.ts
Normal file
111
src/server/utils/app-template.utils.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import { AppTemplateContentModel, AppTemplateInputSettingsModel } from "@/shared/model/app-template.model";
|
||||
import { DatabaseTemplateInfoModel, databaseTemplateInfoZodModel } from "@/shared/model/database-template-info.model";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
import crypto from "crypto";
|
||||
import { EnvVarUtils } from "./env-var.utils";
|
||||
import { KubeObjectNameUtils } from "./kube-object-name.utils";
|
||||
|
||||
export class AppTemplateUtils {
|
||||
static mapTemplateInputValuesToApp(appTemplate: AppTemplateContentModel,
|
||||
inputValues: AppTemplateInputSettingsModel[]) {
|
||||
|
||||
this.populateRandomValues(inputValues);
|
||||
|
||||
const app = { ...appTemplate.appModel };
|
||||
|
||||
const envVariables = inputValues.filter(x => x.isEnvVar);
|
||||
const otherConfigValues = inputValues.filter(x => !x.isEnvVar);
|
||||
|
||||
for (const envVariable of envVariables) {
|
||||
app.envVars += `${envVariable.key}=${envVariable.value}\n`;
|
||||
}
|
||||
|
||||
for (const configValue of otherConfigValues) {
|
||||
(app as any)[configValue.key] = configValue.value;
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces placeholders in the env variables with the database information.
|
||||
*
|
||||
* params:
|
||||
* - {databaseName}
|
||||
* - {username}
|
||||
* - {password}
|
||||
* - {port}
|
||||
* - {hostname}
|
||||
*/
|
||||
static replacePlaceholdersInEnvVariablesWithDatabaseInfo(app: AppExtendedModel, databaseInfo: DatabaseTemplateInfoModel) {
|
||||
app.envVars = app.envVars.replaceAll(/\{databaseName\}/g, databaseInfo.databaseName);
|
||||
app.envVars = app.envVars.replaceAll(/\{username\}/g, databaseInfo.username);
|
||||
app.envVars = app.envVars.replaceAll(/\{password\}/g, databaseInfo.password);
|
||||
app.envVars = app.envVars.replaceAll(/\{port\}/g, databaseInfo.port + '');
|
||||
app.envVars = app.envVars.replaceAll(/\{hostname\}/g, databaseInfo.hostname);
|
||||
}
|
||||
|
||||
static populateRandomValues(inputValues: AppTemplateInputSettingsModel[]) {
|
||||
for (const input of inputValues) {
|
||||
if (input.randomGeneratedIfEmpty && !input.value) {
|
||||
input.value = crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getDatabaseModelFromApp(app: AppExtendedModel): DatabaseTemplateInfoModel {
|
||||
if (app.appType === 'APP') {
|
||||
throw new ServiceException('Cannot retreive database infos from app');
|
||||
}
|
||||
let returnVal: DatabaseTemplateInfoModel;
|
||||
const envVars = EnvVarUtils.parseEnvVariables(app);
|
||||
const port = app.appPorts.find(x => !!x.port)?.port!;
|
||||
const hostname = KubeObjectNameUtils.toServiceName(app.id);
|
||||
if (app.appType === 'MONGODB') {
|
||||
returnVal = {
|
||||
databaseName: envVars.find(x => x.name === 'MONGO_INITDB_DATABASE')?.value!,
|
||||
username: envVars.find(x => x.name === 'MONGO_INITDB_ROOT_USERNAME')?.value!,
|
||||
password: envVars.find(x => x.name === 'MONGO_INITDB_ROOT_PASSWORD')?.value!,
|
||||
port,
|
||||
hostname,
|
||||
};
|
||||
} else if (app.appType === 'MYSQL') {
|
||||
returnVal = {
|
||||
databaseName: envVars.find(x => x.name === 'MYSQL_DATABASE')?.value!,
|
||||
username: envVars.find(x => x.name === 'MYSQL_USER')?.value!,
|
||||
password: envVars.find(x => x.name === 'MYSQL_PASSWORD')?.value!,
|
||||
port,
|
||||
hostname,
|
||||
};
|
||||
} else if (app.appType === 'POSTGRES') {
|
||||
returnVal = {
|
||||
databaseName: envVars.find(x => x.name === 'POSTGRES_DB')?.value!,
|
||||
username: envVars.find(x => x.name === 'POSTGRES_USER')?.value!,
|
||||
password: envVars.find(x => x.name === 'POSTGRES_PASSWORD')?.value!,
|
||||
port,
|
||||
hostname,
|
||||
};
|
||||
} else if (app.appType === 'MARIADB') {
|
||||
returnVal = {
|
||||
databaseName: envVars.find(x => x.name === 'MYSQL_DATABASE')?.value!,
|
||||
username: envVars.find(x => x.name === 'MYSQL_USER')?.value!,
|
||||
password: envVars.find(x => x.name === 'MYSQL_PASSWORD')?.value!,
|
||||
port,
|
||||
hostname,
|
||||
};
|
||||
} else {
|
||||
throw new ServiceException('Unknown database type, could not load database information.');
|
||||
}
|
||||
|
||||
const parseReturn = databaseTemplateInfoZodModel.safeParse(returnVal);
|
||||
if (!parseReturn.success) {
|
||||
console.error('Error parsing database info');
|
||||
console.error('input', app);
|
||||
console.error('database info', returnVal);
|
||||
console.error('errors', parseReturn.error);
|
||||
throw new ServiceException('Error parsing database info');
|
||||
}
|
||||
return returnVal;
|
||||
}
|
||||
}
|
||||
11
src/server/utils/env-var.utils.ts
Normal file
11
src/server/utils/env-var.utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
|
||||
export class EnvVarUtils {
|
||||
static parseEnvVariables(app: AppExtendedModel) {
|
||||
return app.envVars ? app.envVars.split('\n').filter(x => !!x).map(env => {
|
||||
const [name] = env.split('=');
|
||||
const value = env.replace(`${name}=`, '');
|
||||
return { name, value };
|
||||
}) : [];
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const appSourceTypeZodModel = z.enum(["GIT", "CONTAINER"]);
|
||||
export const appTypeZodModel = z.enum(["APP", "POSTGRES", "MYSQL", "MARIADB", "MONGODB"]);
|
||||
|
||||
export const appSourceInfoInputZodModel = z.object({
|
||||
sourceType: appSourceTypeZodModel,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { AppDomainModel, AppModel, AppPortModel, AppVolumeModel, RelatedAppDomainModel, RelatedAppPortModel, RelatedAppVolumeModel } from "./generated-zod";
|
||||
import { appSourceTypeZodModel } from "./app-source-info.model";
|
||||
import { appSourceTypeZodModel, appTypeZodModel } from "./app-source-info.model";
|
||||
import { appVolumeTypeZodModel } from "./volume-edit.model";
|
||||
|
||||
const appModelWithRelations = z.lazy(() => AppModel.extend({
|
||||
projectId: z.undefined(),
|
||||
dockerfilePath: z.undefined(),
|
||||
appType: appTypeZodModel,
|
||||
sourceType: appSourceTypeZodModel,
|
||||
id: z.undefined(),
|
||||
createdAt: z.undefined(),
|
||||
@@ -17,6 +18,7 @@ export const appTemplateInputSettingsZodModel = z.object({
|
||||
label: z.string(),
|
||||
value: z.any(),
|
||||
isEnvVar: z.boolean(),
|
||||
randomGeneratedIfEmpty: z.boolean(),
|
||||
});
|
||||
export type AppTemplateInputSettingsModel = z.infer<typeof appTemplateInputSettingsZodModel>;
|
||||
|
||||
@@ -42,6 +44,7 @@ export type AppTemplateContentModel = z.infer<typeof appTemplateContentZodModel>
|
||||
|
||||
export const appTemplateZodModel = z.object({
|
||||
name: z.string(),
|
||||
iconName: z.string().nullish(),
|
||||
templates: appTemplateContentZodModel.array(),
|
||||
});
|
||||
|
||||
|
||||
14
src/shared/model/database-template-info.model.ts
Normal file
14
src/shared/model/database-template-info.model.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
import { AppDomainModel, AppModel, AppPortModel, AppVolumeModel, RelatedAppDomainModel, RelatedAppPortModel, RelatedAppVolumeModel } from "./generated-zod";
|
||||
import { appSourceTypeZodModel, appTypeZodModel } from "./app-source-info.model";
|
||||
import { appVolumeTypeZodModel } from "./volume-edit.model";
|
||||
|
||||
export const databaseTemplateInfoZodModel = z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
port: z.number(),
|
||||
hostname: z.string(),
|
||||
databaseName: z.string(),
|
||||
});
|
||||
|
||||
export type DatabaseTemplateInfoModel = z.infer<typeof databaseTemplateInfoZodModel>;
|
||||
@@ -5,6 +5,7 @@ import { CompleteProject, RelatedProjectModel, CompleteAppDomain, RelatedAppDoma
|
||||
export const AppModel = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
appType: z.string(),
|
||||
projectId: z.string(),
|
||||
sourceType: z.string(),
|
||||
containerImageSource: z.string().nullish(),
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { AppTemplateModel } from "../model/app-template.model";
|
||||
import { mariadbAppTemplate } from "./mariadb.template";
|
||||
import { wordpressAppTemplate } from "./apps/wordpress.template";
|
||||
import { mariadbAppTemplate } from "./databases/mariadb.template";
|
||||
import { mongodbAppTemplate } from "./databases/mongodb.template";
|
||||
import { mysqlAppTemplate } from "./databases/mysql.template";
|
||||
import { postgreAppTemplate } from "./databases/postgres.template";
|
||||
|
||||
export const allTemplates: AppTemplateModel[] = [
|
||||
mariadbAppTemplate
|
||||
|
||||
export const databaseTemplates: AppTemplateModel[] = [
|
||||
postgreAppTemplate,
|
||||
mongodbAppTemplate,
|
||||
mariadbAppTemplate,
|
||||
mysqlAppTemplate
|
||||
];
|
||||
|
||||
export const appTemplates: AppTemplateModel[] = [
|
||||
wordpressAppTemplate
|
||||
];
|
||||
|
||||
|
||||
export const allTemplates: AppTemplateModel[] = databaseTemplates.concat(appTemplates);
|
||||
86
src/shared/templates/apps/wordpress.template.ts
Normal file
86
src/shared/templates/apps/wordpress.template.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { AppTemplateModel } from "../../model/app-template.model";
|
||||
import { mariadbAppTemplate } from "../databases/mariadb.template";
|
||||
|
||||
export const wordpressAppTemplate: AppTemplateModel = {
|
||||
name: "WordPress",
|
||||
iconName: 'wordpress.png',
|
||||
templates: [{
|
||||
// MariaDB
|
||||
inputSettings: [
|
||||
{
|
||||
key: "containerImageSource",
|
||||
label: "Container Image",
|
||||
value: "mariadb:11",
|
||||
isEnvVar: false,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_PASSWORD",
|
||||
label: "Database Passwort",
|
||||
value: "",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: true,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_ROOT_PASSWORD",
|
||||
label: "Root Password",
|
||||
value: "",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: true,
|
||||
},
|
||||
],
|
||||
appModel: {
|
||||
name: "MariaDb",
|
||||
appType: 'MARIADB',
|
||||
sourceType: 'CONTAINER',
|
||||
containerImageSource: "",
|
||||
replicas: 1,
|
||||
envVars: `MYSQL_DATABASE=wordpress
|
||||
MYSQL_USER=wordpress
|
||||
`,
|
||||
},
|
||||
appDomains: [],
|
||||
appVolumes: [{
|
||||
size: 500,
|
||||
containerMountPath: '/var/lib/mysql',
|
||||
accessMode: 'ReadWriteOnce'
|
||||
}],
|
||||
appPorts: [{
|
||||
port: 3306,
|
||||
}]
|
||||
},
|
||||
// WordPress Backend
|
||||
{
|
||||
inputSettings: [
|
||||
{
|
||||
key: "containerImageSource",
|
||||
label: "Container Image",
|
||||
value: "wordpress:latest",
|
||||
isEnvVar: false,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
],
|
||||
appModel: {
|
||||
name: "WordPress",
|
||||
appType: 'APP',
|
||||
sourceType: 'CONTAINER',
|
||||
containerImageSource: "",
|
||||
replicas: 1,
|
||||
envVars: `WORDPRESS_DB_HOST={hostname}:{port}
|
||||
WORDPRESS_DB_NAME={databaseName}
|
||||
WORDPRESS_DB_USER={username}
|
||||
WORDPRESS_DB_PASSWORD={password}
|
||||
WORDPRESS_TABLE_PREFIX=wp_
|
||||
`,
|
||||
},
|
||||
appDomains: [],
|
||||
appVolumes: [{
|
||||
size: 500,
|
||||
containerMountPath: '/var/www/html',
|
||||
accessMode: 'ReadWriteMany'
|
||||
}],
|
||||
appPorts: [{
|
||||
port: 80,
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,38 +1,51 @@
|
||||
import { AppTemplateModel } from "../model/app-template.model";
|
||||
import { AppTemplateModel } from "../../model/app-template.model";
|
||||
|
||||
export const mariadbAppTemplate: AppTemplateModel = {
|
||||
name: "MariaDB",
|
||||
iconName: 'mariadb.svg',
|
||||
templates: [{
|
||||
inputSettings: [
|
||||
{
|
||||
key: "MYSQL_ROOT_PASSWORD",
|
||||
label: "Root Password",
|
||||
value: "mariadb",
|
||||
isEnvVar: true,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_USER",
|
||||
label: "User",
|
||||
value: "mariadb",
|
||||
isEnvVar: true,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_PASSWORD",
|
||||
label: "Password",
|
||||
value: "mariadb",
|
||||
isEnvVar: true,
|
||||
key: "containerImageSource",
|
||||
label: "Container Image",
|
||||
value: "mariadb:11",
|
||||
isEnvVar: false,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_DATABASE",
|
||||
label: "Database",
|
||||
value: "defaultdb",
|
||||
label: "Database Name",
|
||||
value: "mariadb",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_USER",
|
||||
label: "Database User",
|
||||
value: "mariadbuser",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_PASSWORD",
|
||||
label: "Database Passwort",
|
||||
value: "",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: true,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_ROOT_PASSWORD",
|
||||
label: "Root Password",
|
||||
value: "",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: true,
|
||||
},
|
||||
],
|
||||
appModel: {
|
||||
name: "MariaDb",
|
||||
appType: 'MARIADB',
|
||||
sourceType: 'CONTAINER',
|
||||
containerImageSource: "mariadb:latest",
|
||||
containerImageSource: "",
|
||||
replicas: 1,
|
||||
envVars: ``,
|
||||
},
|
||||
@@ -46,4 +59,4 @@ export const mariadbAppTemplate: AppTemplateModel = {
|
||||
port: 3306,
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
55
src/shared/templates/databases/mongodb.template.ts
Normal file
55
src/shared/templates/databases/mongodb.template.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { AppTemplateModel } from "../../model/app-template.model";
|
||||
|
||||
export const mongodbAppTemplate: AppTemplateModel = {
|
||||
name: "MongoDB",
|
||||
iconName: 'mongodb.svg',
|
||||
templates: [{
|
||||
inputSettings: [
|
||||
{
|
||||
key: "containerImageSource",
|
||||
label: "Container Image",
|
||||
value: "mongo:7",
|
||||
isEnvVar: false,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MONGO_INITDB_DATABASE",
|
||||
label: "Database Name",
|
||||
value: "mongodb",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MONGO_INITDB_ROOT_USERNAME",
|
||||
label: "Username",
|
||||
value: "mongodbuser",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MONGO_INITDB_ROOT_PASSWORD",
|
||||
label: "Password",
|
||||
value: "",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: true,
|
||||
},
|
||||
],
|
||||
appModel: {
|
||||
name: "MongoDB",
|
||||
appType: 'MONGODB',
|
||||
sourceType: 'CONTAINER',
|
||||
containerImageSource: "",
|
||||
replicas: 1,
|
||||
envVars: ``,
|
||||
},
|
||||
appDomains: [],
|
||||
appVolumes: [{
|
||||
size: 500,
|
||||
containerMountPath: '/data/db',
|
||||
accessMode: 'ReadWriteOnce'
|
||||
}],
|
||||
appPorts: [{
|
||||
port: 27017,
|
||||
}]
|
||||
}],
|
||||
};
|
||||
62
src/shared/templates/databases/mysql.template.ts
Normal file
62
src/shared/templates/databases/mysql.template.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { AppTemplateModel } from "../../model/app-template.model";
|
||||
|
||||
export const mysqlAppTemplate: AppTemplateModel = {
|
||||
name: "MySQL",
|
||||
iconName: 'mysql.svg',
|
||||
templates: [{
|
||||
inputSettings: [
|
||||
{
|
||||
key: "containerImageSource",
|
||||
label: "Container Image",
|
||||
value: "mysql:9",
|
||||
isEnvVar: false,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_DATABASE",
|
||||
label: "Database Name",
|
||||
value: "mysqldb",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_USER",
|
||||
label: "Database User",
|
||||
value: "mysqluser",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_PASSWORD",
|
||||
label: "Database Password",
|
||||
value: "",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: true,
|
||||
},
|
||||
{
|
||||
key: "MYSQL_ROOT_PASSWORD",
|
||||
label: "Root Password",
|
||||
value: "",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: true,
|
||||
},
|
||||
],
|
||||
appModel: {
|
||||
name: "MySQL",
|
||||
appType: 'MYSQL',
|
||||
sourceType: 'CONTAINER',
|
||||
containerImageSource: "",
|
||||
replicas: 1,
|
||||
envVars: ``,
|
||||
},
|
||||
appDomains: [],
|
||||
appVolumes: [{
|
||||
size: 500,
|
||||
containerMountPath: '/var/lib/mysql',
|
||||
accessMode: 'ReadWriteOnce'
|
||||
}],
|
||||
appPorts: [{
|
||||
port: 3306,
|
||||
}]
|
||||
}]
|
||||
}
|
||||
55
src/shared/templates/databases/postgres.template.ts
Normal file
55
src/shared/templates/databases/postgres.template.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { AppTemplateModel } from "../../model/app-template.model";
|
||||
|
||||
export const postgreAppTemplate: AppTemplateModel = {
|
||||
name: "PostgreSQL",
|
||||
iconName: 'postgres.svg',
|
||||
templates: [{
|
||||
inputSettings: [
|
||||
{
|
||||
key: "containerImageSource",
|
||||
label: "Container Image",
|
||||
value: "postgres:17",
|
||||
isEnvVar: false,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "POSTGRES_DB",
|
||||
label: "Database Name",
|
||||
value: "postgresdb",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "POSTGRES_USER",
|
||||
label: "Database User",
|
||||
value: "postgresuser",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
{
|
||||
key: "POSTGRES_PASSWORD",
|
||||
label: "Database Password",
|
||||
value: "",
|
||||
isEnvVar: true,
|
||||
randomGeneratedIfEmpty: false,
|
||||
},
|
||||
],
|
||||
appModel: {
|
||||
name: "PostgreSQL",
|
||||
appType: 'POSTGRES',
|
||||
sourceType: 'CONTAINER',
|
||||
containerImageSource: "",
|
||||
replicas: 1,
|
||||
envVars: ``,
|
||||
},
|
||||
appDomains: [],
|
||||
appVolumes: [{
|
||||
size: 500,
|
||||
containerMountPath: '/var/lib/postgresql/data',
|
||||
accessMode: 'ReadWriteOnce'
|
||||
}],
|
||||
appPorts: [{
|
||||
port: 5432,
|
||||
}]
|
||||
}],
|
||||
};
|
||||
Reference in New Issue
Block a user