From e61a5512d11b4aa9bd57c11132000cf9761c97ac Mon Sep 17 00:00:00 2001 From: ianedwards Date: Thu, 4 Jan 2024 15:55:57 -0500 Subject: [PATCH] add redis to preview env (#4119) --- dashboard/package-lock.json | 14 +- dashboard/package.json | 2 +- dashboard/src/assets/redis.svg | 2 + dashboard/src/lib/addons/index.ts | 59 ++++++- dashboard/src/lib/addons/redis.ts | 27 ++++ .../main/home/app-dashboard/apps/Addon.tsx | 18 ++- .../v2/setup-app/PreviewAppDataContainer.tsx | 22 ++- .../main/home/managed-addons/AddonListRow.tsx | 21 ++- .../main/home/managed-addons/AddonsList.tsx | 33 ++-- .../home/managed-addons/tabs/PostgresTabs.tsx | 36 +---- .../home/managed-addons/tabs/RedisTabs.tsx | 149 ++++++++++++++++++ .../main/home/managed-addons/tabs/shared.tsx | 34 ++++ 12 files changed, 336 insertions(+), 81 deletions(-) create mode 100644 dashboard/src/assets/redis.svg create mode 100644 dashboard/src/lib/addons/redis.ts create mode 100644 dashboard/src/main/home/managed-addons/tabs/RedisTabs.tsx create mode 100644 dashboard/src/main/home/managed-addons/tabs/shared.tsx diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 73c8c5efa88..6702653f882 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -95,7 +95,7 @@ "@babel/preset-typescript": "^7.15.0", "@ianvs/prettier-plugin-sort-imports": "^4.1.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", - "@porter-dev/api-contracts": "^0.2.71", + "@porter-dev/api-contracts": "^0.2.81", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", @@ -2754,9 +2754,9 @@ } }, "node_modules/@porter-dev/api-contracts": { - "version": "0.2.71", - "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.71.tgz", - "integrity": "sha512-fVYGLym26GAEcvOw0hPsbhDkge1InN8+a15papXkOA4R/AOmkUsVA1Hn2zgrFosTwF2matXKh+MPYoQVbbXTYA==", + "version": "0.2.81", + "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.81.tgz", + "integrity": "sha512-YrB0P8gbo1z2Eh5iYkU+BWPI6k8mSi22yyOl5K5xkV8L2X22APIdtUSy6gkEck7qrDvuZoxLs74GFsL7gNbvAg==", "dev": true, "dependencies": { "@bufbuild/protobuf": "^1.1.0" @@ -20056,9 +20056,9 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@porter-dev/api-contracts": { - "version": "0.2.71", - "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.71.tgz", - "integrity": "sha512-fVYGLym26GAEcvOw0hPsbhDkge1InN8+a15papXkOA4R/AOmkUsVA1Hn2zgrFosTwF2matXKh+MPYoQVbbXTYA==", + "version": "0.2.81", + "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.81.tgz", + "integrity": "sha512-YrB0P8gbo1z2Eh5iYkU+BWPI6k8mSi22yyOl5K5xkV8L2X22APIdtUSy6gkEck7qrDvuZoxLs74GFsL7gNbvAg==", "dev": true, "requires": { "@bufbuild/protobuf": "^1.1.0" diff --git a/dashboard/package.json b/dashboard/package.json index b28e17ff2e2..4f23e024ef9 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -102,7 +102,7 @@ "@babel/preset-typescript": "^7.15.0", "@ianvs/prettier-plugin-sort-imports": "^4.1.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", - "@porter-dev/api-contracts": "^0.2.71", + "@porter-dev/api-contracts": "^0.2.81", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", diff --git a/dashboard/src/assets/redis.svg b/dashboard/src/assets/redis.svg new file mode 100644 index 00000000000..ed312206b9f --- /dev/null +++ b/dashboard/src/assets/redis.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/dashboard/src/lib/addons/index.ts b/dashboard/src/lib/addons/index.ts index 9fa240b8284..1ae75c46ec2 100644 --- a/dashboard/src/lib/addons/index.ts +++ b/dashboard/src/lib/addons/index.ts @@ -8,9 +8,10 @@ import { z } from "zod"; import { serviceStringValidator } from "lib/porter-apps/values"; import { defaultPostgresAddon, postgresConfigValidator } from "./postgres"; +import { redisConfigValidator } from "./redis"; export const clientAddonValidator = z.object({ - expanded: z.boolean().default(true), + expanded: z.boolean().default(false), canDelete: z.boolean().default(true), name: z.object({ readOnly: z.boolean(), @@ -23,20 +24,40 @@ export const clientAddonValidator = z.object({ }), }), envGroups: z.array(serviceStringValidator).default([]), - config: z.discriminatedUnion("type", [postgresConfigValidator]), + config: z.discriminatedUnion("type", [ + postgresConfigValidator, + redisConfigValidator, + ]), }); export type ClientAddon = z.infer; -export function defaultClientAddon(): ClientAddon { - return clientAddonValidator.parse({ - name: { readOnly: false, value: "addon" }, - config: defaultPostgresAddon(), - }); +export function defaultClientAddon( + type: ClientAddon["config"]["type"] +): ClientAddon { + return match(type) + .with("postgres", () => + clientAddonValidator.parse({ + expanded: true, + name: { readOnly: false, value: "addon" }, + config: defaultPostgresAddon(), + }) + ) + .with("redis", () => + clientAddonValidator.parse({ + expanded: true, + name: { readOnly: false, value: "addon" }, + config: redisConfigValidator.parse({ + type: "redis", + }), + }) + ) + .exhaustive(); } function addonTypeEnumProto(type: ClientAddon["config"]["type"]): AddonType { return match(type) .with("postgres", () => AddonType.POSTGRES) + .with("redis", () => AddonType.REDIS) .exhaustive(); } @@ -50,6 +71,14 @@ export function clientAddonToProto(addon: ClientAddon): Addon { }, case: "postgres" as const, })) + .with({ type: "redis" }, (data) => ({ + value: { + cpuCores: data.cpuCores.value, + ramMegabytes: data.ramMegabytes.value, + storageGigabytes: data.storageGigabytes.value, + }, + case: "redis" as const, + })) .exhaustive(); const proto = new Addon({ @@ -95,6 +124,22 @@ export function clientAddonFromProto({ username: variables.POSTGRESQL_USERNAME, password: secrets.POSTGRESQL_PASSWORD, })) + .with({ case: "redis" }, (data) => ({ + type: "redis" as const, + cpuCores: { + readOnly: false, + value: data.value.cpuCores, + }, + ramMegabytes: { + readOnly: false, + value: data.value.ramMegabytes, + }, + storageGigabytes: { + readOnly: false, + value: data.value.storageGigabytes, + }, + password: secrets.REDIS_PASSWORD, + })) .exhaustive(); const clientAddon = clientAddonValidator.parse({ diff --git a/dashboard/src/lib/addons/redis.ts b/dashboard/src/lib/addons/redis.ts new file mode 100644 index 00000000000..abbf55fbc11 --- /dev/null +++ b/dashboard/src/lib/addons/redis.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +import { serviceNumberValidator } from "lib/porter-apps/values"; + +export const redisConfigValidator = z.object({ + type: z.literal("redis"), + cpuCores: serviceNumberValidator.default({ + value: 0.5, + readOnly: false, + }), + ramMegabytes: serviceNumberValidator.default({ + value: 512, + readOnly: false, + }), + storageGigabytes: serviceNumberValidator.default({ + value: 1, + readOnly: false, + }), + password: z.string().default("redis"), +}); +export type RedisConfig = z.infer; + +export function defaultRedisAddon(): RedisConfig { + return redisConfigValidator.parse({ + type: "redis", + }); +} diff --git a/dashboard/src/main/home/app-dashboard/apps/Addon.tsx b/dashboard/src/main/home/app-dashboard/apps/Addon.tsx index e77a1516d29..95395d70ab1 100644 --- a/dashboard/src/main/home/app-dashboard/apps/Addon.tsx +++ b/dashboard/src/main/home/app-dashboard/apps/Addon.tsx @@ -12,6 +12,7 @@ import { type ClientAddon } from "lib/addons"; import { useDeploymentTarget } from "shared/DeploymentTargetContext"; import copy from "assets/copy-left.svg"; import postgresql from "assets/postgresql.svg"; +import redis from "assets/redis.svg"; import { Block, Row } from "./AppGrid"; @@ -27,15 +28,26 @@ export const Addon: React.FC = ({ addon, view }) => { if (!currentDeploymentTarget) return ""; if (!addon.name.value) return ""; - return `${addon.name.value}-postgres.${currentDeploymentTarget.namespace}.svc.cluster.local:5432`; + const port = match(addon.config.type) + .with("postgres", () => 5432) + .with("redis", () => 6379) + .exhaustive(); + + return `${addon.name.value}-${addon.config.type}.${currentDeploymentTarget.namespace}.svc.cluster.local:${port}`; }, [currentDeploymentTarget, addon.name.value]); + const renderIcon = (type: ClientAddon["config"]["type"]): JSX.Element => + match(type) + .with("postgres", () => ) + .with("redis", () => ) + .exhaustive(); + return match(view) .with("grid", () => ( - + {renderIcon(addon.config.type)} {addon.name.value} @@ -60,7 +72,7 @@ export const Addon: React.FC = ({ addon, view }) => { - + {renderIcon(addon.config.type)} {addon.name.value} diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx index 009dcb26320..5e6f1db0f98 100644 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx +++ b/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx @@ -189,16 +189,22 @@ export const PreviewAppDataContainer: React.FC = ({ setValidatedAppProto(proto); const addons = data.addons.map((addon) => { - const variables = match(addon.config.type) - .with("postgres", () => ({ - POSTGRESQL_USERNAME: addon.config.username, + const variables = match(addon.config) + .with({ type: "postgres" }, (conf) => ({ + POSTGRESQL_USERNAME: conf.username, })) - .otherwise(() => ({})); - const secrets = match(addon.config.type) - .with("postgres", () => ({ - POSTGRESQL_PASSWORD: addon.config.password, + .with({ type: "redis" }, (conf) => ({ + REDIS_PASSWORD: conf.password, })) - .otherwise(() => ({})); + .exhaustive(); + const secrets = match(addon.config) + .with({ type: "postgres" }, (conf) => ({ + POSTGRESQL_PASSWORD: conf.password, + })) + .with({ type: "redis" }, (conf) => ({ + REDIS_PASSWORD: conf.password, + })) + .exhaustive(); const proto = clientAddonToProto(addon); diff --git a/dashboard/src/main/home/managed-addons/AddonListRow.tsx b/dashboard/src/main/home/managed-addons/AddonListRow.tsx index 50e6deea223..2a278bce96c 100644 --- a/dashboard/src/main/home/managed-addons/AddonListRow.tsx +++ b/dashboard/src/main/home/managed-addons/AddonListRow.tsx @@ -4,12 +4,15 @@ import { type UseFieldArrayUpdate } from "react-hook-form"; import styled from "styled-components"; import { match } from "ts-pattern"; +import Spacer from "components/porter/Spacer"; import { type ClientAddon } from "lib/addons"; import postgresql from "assets/postgresql.svg"; +import redis from "assets/redis.svg"; import { type AppTemplateFormData } from "../cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer"; import { PostgresTabs } from "./tabs/PostgresTabs"; +import { RedisTabs } from "./tabs/RedisTabs"; type AddonRowProps = { index: number; @@ -24,7 +27,11 @@ export const AddonListRow: React.FC = ({ update, remove, }) => { - const renderIcon = (): JSX.Element => ; + const renderIcon = (type: ClientAddon["config"]["type"]): JSX.Element => + match(type) + .with("postgres", () => ) + .with("redis", () => ) + .exhaustive(); return ( <> @@ -42,7 +49,7 @@ export const AddonListRow: React.FC = ({ arrow_drop_down - {renderIcon()} + {renderIcon(addon.config.type)} {addon.name.value.trim().length > 0 ? addon.name.value : "New Addon"} @@ -84,15 +91,19 @@ export const AddonListRow: React.FC = ({ border: "1px solid #494b4f", }} > - {match(addon.config.type) - .with("postgres", () => ( - + {match(addon) + .with({ config: { type: "postgres" } }, (ao) => ( + + )) + .with({ config: { type: "redis" } }, (ao) => ( + )) .exhaustive()} )} + ); }; diff --git a/dashboard/src/main/home/managed-addons/AddonsList.tsx b/dashboard/src/main/home/managed-addons/AddonsList.tsx index 700432d4afa..aeba8261bac 100644 --- a/dashboard/src/main/home/managed-addons/AddonsList.tsx +++ b/dashboard/src/main/home/managed-addons/AddonsList.tsx @@ -21,6 +21,7 @@ import { type AppTemplateFormData } from "main/home/cluster-dashboard/preview-en import { defaultClientAddon } from "lib/addons"; import postgresql from "assets/postgresql.svg"; +import redis from "assets/redis.svg"; import { AddonListRow } from "./AddonListRow"; @@ -32,7 +33,7 @@ const addAddonFormValidator = z.object({ .regex(/^[a-z0-9-]+$/, { message: 'Lowercase letters, numbers, and " - " only.', }), - type: z.enum(["postgres"]), + type: z.enum(["postgres", "redis"]), }); type AddAddonFormValues = z.infer; @@ -80,7 +81,7 @@ export const AddonsList: React.FC = () => { }, [fields]); const onSubmit = handleSubmit((data) => { - const baseAddon = defaultClientAddon(); + const baseAddon = defaultClientAddon(data.type); append({ ...baseAddon, name: { @@ -106,19 +107,15 @@ export const AddonsList: React.FC = () => { /> ))} - {fields.length === 0 && ( - <> - { - setShowAddAddonModal(true); - }} - > - add - Include add-on in preview environments - - - - )} + { + setShowAddAddonModal(true); + }} + > + add + Include add-on in preview environments + + {showAddAddonModal && ( { @@ -134,6 +131,7 @@ export const AddonsList: React.FC = () => { {match(addonType) .with("postgres", () => ) + .with("redis", () => ) .exhaustive()} { setValue={(value: string) => { onChange(value); }} - options={[{ label: "Postgres", value: "postgres" }]} + options={[ + { label: "Postgres", value: "postgres" }, + { label: "Redis", value: "redis" }, + ]} /> )} /> diff --git a/dashboard/src/main/home/managed-addons/tabs/PostgresTabs.tsx b/dashboard/src/main/home/managed-addons/tabs/PostgresTabs.tsx index cf5e1eb7846..a8ea2bac844 100644 --- a/dashboard/src/main/home/managed-addons/tabs/PostgresTabs.tsx +++ b/dashboard/src/main/home/managed-addons/tabs/PostgresTabs.tsx @@ -1,6 +1,5 @@ import React, { useMemo, useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; -import styled from "styled-components"; import { match } from "ts-pattern"; import CopyToClipboard from "components/CopyToClipboard"; @@ -15,6 +14,8 @@ import { type ClientAddon } from "lib/addons"; import { useClusterResources } from "shared/ClusterResourcesContext"; import copy from "assets/copy-left.svg"; +import { Code, CopyContainer, CopyIcon, IdContainer } from "./shared"; + type Props = { index: number; addon: ClientAddon & { @@ -156,36 +157,3 @@ export const PostgresTabs: React.FC = ({ index }) => { ); }; - -const CopyIcon = styled.img` - cursor: pointer; - margin-left: 5px; - margin-right: 5px; - width: 15px; - height: 15px; - :hover { - opacity: 0.8; - } -`; - -const Code = styled.span` - font-family: monospace; -`; - -const IdContainer = styled.div` - background: #26292e; - border-radius: 5px; - padding: 10px; - display: flex; - width: 550px; - border-radius: 5px; - border: 1px solid ${({ theme }) => theme.border}; - align-items: center; - user-select: text; -`; - -const CopyContainer = styled.div` - display: flex; - align-items: center; - margin-left: auto; -`; diff --git a/dashboard/src/main/home/managed-addons/tabs/RedisTabs.tsx b/dashboard/src/main/home/managed-addons/tabs/RedisTabs.tsx new file mode 100644 index 00000000000..8bc780e0c2f --- /dev/null +++ b/dashboard/src/main/home/managed-addons/tabs/RedisTabs.tsx @@ -0,0 +1,149 @@ +import React, { useMemo, useState } from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import { match } from "ts-pattern"; + +import CopyToClipboard from "components/CopyToClipboard"; +import { ControlledInput } from "components/porter/ControlledInput"; +import Spacer from "components/porter/Spacer"; +import Text from "components/porter/Text"; +import TabSelector from "components/TabSelector"; +import IntelligentSlider from "main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider"; +import { type AppTemplateFormData } from "main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer"; +import { type ClientAddon } from "lib/addons"; + +import { useClusterResources } from "shared/ClusterResourcesContext"; +import copy from "assets/copy-left.svg"; + +import { Code, CopyContainer, CopyIcon, IdContainer } from "./shared"; + +type Props = { + index: number; + addon: ClientAddon & { + config: { + type: "redis"; + }; + }; +}; + +export const RedisTabs: React.FC = ({ index }) => { + const { register, control, watch } = useFormContext(); + const { + currentClusterResources: { maxCPU, maxRAM }, + } = useClusterResources(); + + const [currentTab, setCurrentTab] = useState<"credentials" | "resources">( + "credentials" + ); + + const name = watch(`addons.${index}.name`); + const password = watch(`addons.${index}.config.password`); + + const redisURL = useMemo(() => { + if (!password || !name.value) { + return ""; + } + + return `redis://:${password}@${name.value}-redis:6379`; + }, [password, name.value]); + + return ( + <> + + + {match(currentTab) + .with("credentials", () => ( + <> + Redis Password + + + + {redisURL && ( + <> + Internal Redis URL: + + + {redisURL} + + + + + + + + + )} + + )) + .with("resources", () => ( + <> + ( + { + onChange({ + ...value, + value: e, + }); + }} + step={0.1} + disabled={value.readOnly} + disabledTooltip={ + "You may only edit this field in your porter.yaml." + } + isSmartOptimizationOn={false} + decimalsToRoundTo={2} + /> + )} + /> + + ( + { + onChange({ + ...value, + value: e, + }); + }} + step={10} + disabled={value.readOnly} + disabledTooltip={ + "You may only edit this field in your porter.yaml." + } + isSmartOptimizationOn={false} + /> + )} + /> + + )) + .exhaustive()} + + ); +}; diff --git a/dashboard/src/main/home/managed-addons/tabs/shared.tsx b/dashboard/src/main/home/managed-addons/tabs/shared.tsx new file mode 100644 index 00000000000..5b7a20a15e9 --- /dev/null +++ b/dashboard/src/main/home/managed-addons/tabs/shared.tsx @@ -0,0 +1,34 @@ +import styled from "styled-components"; + +export const CopyIcon = styled.img` + cursor: pointer; + margin-left: 5px; + margin-right: 5px; + width: 15px; + height: 15px; + :hover { + opacity: 0.8; + } +`; + +export const Code = styled.span` + font-family: monospace; +`; + +export const IdContainer = styled.div` + background: #26292e; + border-radius: 5px; + padding: 10px; + display: flex; + width: 550px; + border-radius: 5px; + border: 1px solid ${({ theme }) => theme.border}; + align-items: center; + user-select: text; +`; + +export const CopyContainer = styled.div` + display: flex; + align-items: center; + margin-left: auto; +`;