Procházet zdrojové kódy

add redis to preview env (#4119)

ianedwards před 2 roky
rodič
revize
e61a5512d1

+ 7 - 7
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"

+ 1 - 1
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",

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
dashboard/src/assets/redis.svg


+ 52 - 7
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<typeof clientAddonValidator>;
 
-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({

+ 27 - 0
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<typeof redisConfigValidator>;
+
+export function defaultRedisAddon(): RedisConfig {
+  return redisConfigValidator.parse({
+    type: "redis",
+  });
+}

+ 15 - 3
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<AddonProps> = ({ 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", () => <Icon height="16px" src={postgresql} />)
+      .with("redis", () => <Icon height="16px" src={redis} />)
+      .exhaustive();
+
   return match(view)
     .with("grid", () => (
       <Block locked>
         <Container row>
           <Spacer inline width="1px" />
-          <Icon height="16px" src={postgresql} />
+          {renderIcon(addon.config.type)}
           <Spacer inline width="12px" />
           <Text size={14}>{addon.name.value}</Text>
           <Spacer inline x={2} />
@@ -60,7 +72,7 @@ export const Addon: React.FC<AddonProps> = ({ addon, view }) => {
       <Row locked>
         <Container row>
           <Spacer inline width="1px" />
-          <Icon height="16px" src={postgresql} />
+          {renderIcon(addon.config.type)}
           <Spacer inline width="12px" />
           <Text size={14}>{addon.name.value}</Text>
           <Spacer inline x={1} />

+ 14 - 8
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx

@@ -189,16 +189,22 @@ export const PreviewAppDataContainer: React.FC<Props> = ({
       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);
 

+ 16 - 5
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<AddonRowProps> = ({
   update,
   remove,
 }) => {
-  const renderIcon = (): JSX.Element => <Icon src={postgresql} />;
+  const renderIcon = (type: ClientAddon["config"]["type"]): JSX.Element =>
+    match(type)
+      .with("postgres", () => <Icon src={postgresql} />)
+      .with("redis", () => <Icon src={redis} />)
+      .exhaustive();
 
   return (
     <>
@@ -42,7 +49,7 @@ export const AddonListRow: React.FC<AddonRowProps> = ({
           <ActionButton>
             <span className="material-icons dropdown">arrow_drop_down</span>
           </ActionButton>
-          {renderIcon()}
+          {renderIcon(addon.config.type)}
           {addon.name.value.trim().length > 0 ? addon.name.value : "New Addon"}
         </AddonTitle>
 
@@ -84,15 +91,19 @@ export const AddonListRow: React.FC<AddonRowProps> = ({
                 border: "1px solid #494b4f",
               }}
             >
-              {match(addon.config.type)
-                .with("postgres", () => (
-                  <PostgresTabs index={index} addon={addon} />
+              {match(addon)
+                .with({ config: { type: "postgres" } }, (ao) => (
+                  <PostgresTabs index={index} addon={ao} />
+                ))
+                .with({ config: { type: "redis" } }, (ao) => (
+                  <RedisTabs index={index} addon={ao} />
                 ))
                 .exhaustive()}
             </div>
           </StyledSourceBox>
         )}
       </AnimatePresence>
+      <Spacer y={0.5} />
     </>
   );
 };

+ 17 - 16
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<typeof addAddonFormValidator>;
 
@@ -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 = () => {
           />
         ))}
       </AddonsContainer>
-      {fields.length === 0 && (
-        <>
-          <AddAddonButton
-            onClick={() => {
-              setShowAddAddonModal(true);
-            }}
-          >
-            <I className="material-icons add-icon">add</I>
-            Include add-on in preview environments
-          </AddAddonButton>
-          <Spacer y={0.5} />
-        </>
-      )}
+      <AddAddonButton
+        onClick={() => {
+          setShowAddAddonModal(true);
+        }}
+      >
+        <I className="material-icons add-icon">add</I>
+        Include add-on in preview environments
+      </AddAddonButton>
+      <Spacer y={0.5} />
       {showAddAddonModal && (
         <Modal
           closeModal={() => {
@@ -134,6 +131,7 @@ export const AddonsList: React.FC = () => {
             <AddonIcon>
               {match(addonType)
                 .with("postgres", () => <img src={postgresql} />)
+                .with("redis", () => <img src={redis} />)
                 .exhaustive()}
             </AddonIcon>
             <Controller
@@ -146,7 +144,10 @@ export const AddonsList: React.FC = () => {
                   setValue={(value: string) => {
                     onChange(value);
                   }}
-                  options={[{ label: "Postgres", value: "postgres" }]}
+                  options={[
+                    { label: "Postgres", value: "postgres" },
+                    { label: "Redis", value: "redis" },
+                  ]}
                 />
               )}
             />

+ 2 - 34
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<Props> = ({ 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;
-`;

+ 149 - 0
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<Props> = ({ index }) => {
+  const { register, control, watch } = useFormContext<AppTemplateFormData>();
+  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 (
+    <>
+      <TabSelector
+        options={[
+          { label: "Credentials", value: "credentials" },
+          { label: "Resources", value: "resources" },
+        ]}
+        currentTab={currentTab}
+        setCurrentTab={setCurrentTab}
+      />
+      <Spacer y={1} />
+      {match(currentTab)
+        .with("credentials", () => (
+          <>
+            <Text color="helper">Redis Password</Text>
+            <Spacer y={0.25} />
+            <ControlledInput
+              type="text"
+              width="300px"
+              {...register(`addons.${index}.config.password`)}
+            />
+            <Spacer y={1} />
+            {redisURL && (
+              <>
+                <Text color="helper">Internal Redis URL:</Text>
+                <Spacer y={0.5} />
+                <IdContainer>
+                  <Code>{redisURL}</Code>
+                  <CopyContainer>
+                    <CopyToClipboard text={redisURL}>
+                      <CopyIcon src={copy} alt="copy" />
+                    </CopyToClipboard>
+                  </CopyContainer>
+                </IdContainer>
+                <Spacer y={0.5} />
+              </>
+            )}
+          </>
+        ))
+        .with("resources", () => (
+          <>
+            <Controller
+              name={`addons.${index}.config.cpuCores`}
+              control={control}
+              render={({ field: { value, onChange } }) => (
+                <IntelligentSlider
+                  label="CPUs: "
+                  unit="Cores"
+                  min={0.01}
+                  max={maxCPU}
+                  color={"#3f51b5"}
+                  value={value.value.toString()}
+                  setValue={(e) => {
+                    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}
+                />
+              )}
+            />
+            <Spacer y={1} />
+            <Controller
+              name={`addons.${index}.config.ramMegabytes`}
+              control={control}
+              render={({ field: { value, onChange } }) => (
+                <IntelligentSlider
+                  label="RAM: "
+                  unit="MB"
+                  min={1}
+                  max={maxRAM}
+                  color={"#3f51b5"}
+                  value={value.value.toString()}
+                  setValue={(e) => {
+                    onChange({
+                      ...value,
+                      value: e,
+                    });
+                  }}
+                  step={10}
+                  disabled={value.readOnly}
+                  disabledTooltip={
+                    "You may only edit this field in your porter.yaml."
+                  }
+                  isSmartOptimizationOn={false}
+                />
+              )}
+            />
+          </>
+        ))
+        .exhaustive()}
+    </>
+  );
+};

+ 34 - 0
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;
+`;

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů