فهرست منبع

genericize types, add controlled input password view (#4591)

Feroze Mohideen 2 سال پیش
والد
کامیت
5099d3adc1

+ 1 - 0
dashboard/src/assets/eye-off.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-off"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/></svg>

+ 1 - 0
dashboard/src/assets/eye.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>

+ 42 - 18
dashboard/src/components/porter/ControlledInput.tsx

@@ -1,5 +1,12 @@
-import React from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
+
+import eyeOff from "assets/eye-off.svg";
+import eye from "assets/eye.svg";
+
+import Container from "./Container";
+import Icon from "./Icon";
+import Spacer from "./Spacer";
 import Tooltip from "./Tooltip";
 
 /*
@@ -45,6 +52,11 @@ export const ControlledInput = React.forwardRef<
     },
     ref
   ) => {
+    const [isVisible, setIsVisible] = useState(false);
+    const toggleVisibility = (): void => {
+      setIsVisible(!isVisible);
+    };
+
     return disabled && disabledTooltip ? (
       <Tooltip content={disabledTooltip} position="right">
         <Block width={width}>
@@ -72,29 +84,41 @@ export const ControlledInput = React.forwardRef<
         </Block>
       </Tooltip>
     ) : (
-      <Block width={width}>
-        {label && <Label>{label}</Label>}
-        <StyledInput
-          name={name}
-          type={type}
-          autoComplete={autoComplete}
-          placeholder={placeholder}
-          defaultValue={defaultValue}
-          onChange={onChange}
-          onBlur={onBlur}
-          ref={ref}
-          disabled={disabled}
-          width={width}
-          height={height}
-          hasError={(error && true) || error === ""}
-        />
+      <div>
+        <Container row>
+          <Block width={width}>
+            {label && <Label>{label}</Label>}
+            <StyledInput
+              name={name}
+              type={type === "password" && !isVisible ? "password" : "text"}
+              autoComplete={autoComplete}
+              placeholder={placeholder}
+              defaultValue={defaultValue}
+              onChange={onChange}
+              onBlur={onBlur}
+              ref={ref}
+              disabled={disabled}
+              width={width}
+              height={height}
+              hasError={(error && true) || error === ""}
+            />
+          </Block>
+          {type === "password" && (
+            <>
+              <Spacer inline x={0.5} />
+              <div onClick={toggleVisibility} style={{ cursor: "pointer" }}>
+                <Icon src={isVisible ? eyeOff : eye} />
+              </div>
+            </>
+          )}
+        </Container>
         {error && (
           <Error>
             <i className="material-icons">error</i>
             {error}
           </Error>
         )}
-      </Block>
+      </div>
     );
   }
 );

+ 4 - 1
dashboard/src/lib/addons/index.ts

@@ -57,8 +57,11 @@ export const clientAddonValidator = z.object({
     tailscaleConfigValidator,
   ]),
 });
+export type ClientAddonType = z.infer<
+  typeof clientAddonValidator
+>["config"]["type"];
 export type ClientAddon = z.infer<typeof clientAddonValidator> & {
-  template: AddonTemplate;
+  template: AddonTemplate<ClientAddonType>;
 };
 export const legacyAddonValidator = z.object({
   name: z.string(),

+ 1 - 1
dashboard/src/lib/addons/metabase.ts

@@ -8,7 +8,7 @@ export const metabaseConfigValidator = z.object({
   datastore: z
     .object({
       host: z.string().nonempty(),
-      port: z.number(),
+      port: z.coerce.number(),
       databaseName: z.string().nonempty(),
       username: z.string().nonempty(),
       password: z.string().nonempty(),

+ 96 - 17
dashboard/src/lib/addons/template.ts

@@ -7,7 +7,7 @@ import NewRelicForm from "main/home/add-on-dashboard/newrelic/NewRelicForm";
 import TailscaleForm from "main/home/add-on-dashboard/tailscale/TailscaleForm";
 import TailscaleOverview from "main/home/add-on-dashboard/tailscale/TailscaleOverview";
 
-import { type ClientAddon } from ".";
+import { type ClientAddon, type ClientAddonType } from ".";
 
 export type AddonTemplateTag =
   | "Monitoring"
@@ -39,34 +39,68 @@ export const DEFAULT_ADDON_TAB = {
   component: () => null,
 };
 
-export type AddonTemplate = {
-  type: ClientAddon["config"]["type"];
+export type AddonTemplate<T extends ClientAddonType> = {
+  type: T;
   displayName: string;
   description: string;
   icon: string;
   tags: AddonTemplateTag[];
   tabs: AddonTab[]; // this what is rendered on the dashboard after the addon is deployed
+  defaultValues: ClientAddon["config"] & { type: T };
 };
 
-export const ADDON_TEMPLATE_REDIS: AddonTemplate = {
+export const ADDON_TEMPLATE_REDIS: AddonTemplate<"redis"> = {
   type: "redis",
   displayName: "Redis",
   description: "An in-memory database that persists on disk.",
   icon: "https://cdn4.iconfinder.com/data/icons/redis-2/1451/Untitled-2-512.png",
   tags: ["Database"],
   tabs: [],
+  defaultValues: {
+    type: "redis",
+    cpuCores: {
+      value: 0.5,
+      readOnly: false,
+    },
+    ramMegabytes: {
+      value: 512,
+      readOnly: false,
+    },
+    storageGigabytes: {
+      value: 1,
+      readOnly: false,
+    },
+    password: "",
+  },
 };
 
-export const ADDON_TEMPLATE_POSTGRES: AddonTemplate = {
+export const ADDON_TEMPLATE_POSTGRES: AddonTemplate<"postgres"> = {
   type: "postgres",
   displayName: "Postgres",
   description: "An object-relational database system.",
   icon: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/postgresql/postgresql-original.svg",
   tags: ["Database"],
   tabs: [],
+  defaultValues: {
+    type: "postgres",
+    cpuCores: {
+      value: 0.5,
+      readOnly: false,
+    },
+    ramMegabytes: {
+      value: 512,
+      readOnly: false,
+    },
+    storageGigabytes: {
+      value: 1,
+      readOnly: false,
+    },
+    username: "postgres",
+    password: "postgres",
+  },
 };
 
-export const ADDON_TEMPLATE_DATADOG: AddonTemplate = {
+export const ADDON_TEMPLATE_DATADOG: AddonTemplate<"datadog"> = {
   type: "datadog",
   displayName: "DataDog",
   description:
@@ -91,9 +125,19 @@ export const ADDON_TEMPLATE_DATADOG: AddonTemplate = {
       component: Settings,
     },
   ],
+  defaultValues: {
+    type: "datadog",
+    cpuCores: 0.5,
+    ramMegabytes: 512,
+    site: "datadoghq.com",
+    apiKey: "",
+    loggingEnabled: false,
+    apmEnabled: false,
+    dogstatsdEnabled: false,
+  },
 };
 
-export const ADDON_TEMPLATE_MEZMO: AddonTemplate = {
+export const ADDON_TEMPLATE_MEZMO: AddonTemplate<"mezmo"> = {
   type: "mezmo",
   displayName: "Mezmo",
   description: "A popular logging management system.",
@@ -117,9 +161,13 @@ export const ADDON_TEMPLATE_MEZMO: AddonTemplate = {
       component: Settings,
     },
   ],
+  defaultValues: {
+    type: "mezmo",
+    ingestionKey: "",
+  },
 };
 
-export const ADDON_TEMPLATE_METABASE: AddonTemplate = {
+export const ADDON_TEMPLATE_METABASE: AddonTemplate<"metabase"> = {
   type: "metabase",
   displayName: "Metabase",
   description: "An open-source business intelligence tool.",
@@ -142,9 +190,22 @@ export const ADDON_TEMPLATE_METABASE: AddonTemplate = {
       component: Settings,
     },
   ],
+  defaultValues: {
+    type: "metabase",
+    exposedToExternalTraffic: true,
+    porterDomain: "",
+    customDomain: "",
+    datastore: {
+      host: "",
+      port: 0,
+      databaseName: "",
+      username: "",
+      password: "",
+    },
+  },
 };
 
-export const ADDON_TEMPLATE_NEWRELIC: AddonTemplate = {
+export const ADDON_TEMPLATE_NEWRELIC: AddonTemplate<"newrelic"> = {
   type: "newrelic",
   displayName: "New Relic",
   description: "Monitor your applications and infrastructure.",
@@ -168,9 +229,21 @@ export const ADDON_TEMPLATE_NEWRELIC: AddonTemplate = {
       component: Settings,
     },
   ],
+  defaultValues: {
+    type: "newrelic",
+    licenseKey: "",
+    insightsKey: "",
+    personalApiKey: "",
+    accountId: "",
+    loggingEnabled: false,
+    kubeEventsEnabled: false,
+    metricsAdapterEnabled: false,
+    prometheusEnabled: false,
+    pixieEnabled: false,
+  },
 };
 
-export const ADDON_TEMPLATE_TAILSCALE: AddonTemplate = {
+export const ADDON_TEMPLATE_TAILSCALE: AddonTemplate<"tailscale"> = {
   type: "tailscale",
   displayName: "Tailscale",
   description: "A VPN for your applications and datastores.",
@@ -199,12 +272,18 @@ export const ADDON_TEMPLATE_TAILSCALE: AddonTemplate = {
       component: Settings,
     },
   ],
+  defaultValues: {
+    type: "tailscale",
+    authKey: "",
+    subnetRoutes: [],
+  },
 };
 
-export const SUPPORTED_ADDON_TEMPLATES: AddonTemplate[] = [
-  ADDON_TEMPLATE_DATADOG,
-  ADDON_TEMPLATE_MEZMO,
-  ADDON_TEMPLATE_METABASE,
-  ADDON_TEMPLATE_NEWRELIC,
-  ADDON_TEMPLATE_TAILSCALE,
-];
+export const SUPPORTED_ADDON_TEMPLATES: Array<AddonTemplate<ClientAddonType>> =
+  [
+    ADDON_TEMPLATE_DATADOG,
+    ADDON_TEMPLATE_MEZMO,
+    ADDON_TEMPLATE_METABASE,
+    ADDON_TEMPLATE_NEWRELIC,
+    ADDON_TEMPLATE_TAILSCALE,
+  ];

+ 12 - 5
dashboard/src/main/home/add-on-dashboard/AddonForm.tsx

@@ -9,7 +9,7 @@ import { ControlledInput } from "components/porter/ControlledInput";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import VerticalSteps from "components/porter/VerticalSteps";
-import { defaultClientAddon, type ClientAddon } from "lib/addons";
+import { type ClientAddon, type ClientAddonType } from "lib/addons";
 import { type AddonTemplate } from "lib/addons/template";
 import { useAddonList } from "lib/hooks/useAddon";
 import { useDefaultDeploymentTarget } from "lib/hooks/useDeploymentTarget";
@@ -20,10 +20,12 @@ import DashboardHeader from "../cluster-dashboard/DashboardHeader";
 import ClusterContextProvider from "../infrastructure-dashboard/ClusterContextProvider";
 import Configuration from "./common/Configuration";
 
-type Props = {
-  template: AddonTemplate;
+type Props<T extends ClientAddonType> = {
+  template: AddonTemplate<T>;
 };
-const AddonForm: React.FC<Props> = ({ template }) => {
+const AddonForm = <T extends ClientAddonType>({
+  template,
+}: Props<T>): JSX.Element => {
   const { currentProject, currentCluster } = useContext(Context);
   const { defaultDeploymentTarget, isDefaultDeploymentTargetLoading } =
     useDefaultDeploymentTarget();
@@ -56,7 +58,12 @@ const AddonForm: React.FC<Props> = ({ template }) => {
   }, [watchName]);
 
   useEffect(() => {
-    reset(defaultClientAddon(template.type));
+    reset({
+      expanded: true,
+      name: { readOnly: false, value: template.type },
+      config: template.defaultValues,
+      template,
+    });
   }, [template]);
 
   useEffect(() => {

+ 30 - 25
dashboard/src/main/home/add-on-dashboard/AddonTemplates.tsx

@@ -4,6 +4,7 @@ import styled from "styled-components";
 
 import Back from "components/porter/Back";
 import Spacer from "components/porter/Spacer";
+import { type ClientAddonType } from "lib/addons";
 import {
   AddonTemplateTagColor,
   SUPPORTED_ADDON_TEMPLATES,
@@ -47,31 +48,35 @@ const AddonTemplates: React.FC = () => {
         disableLineBreak
       />
       <TemplateListWrapper>
-        {SUPPORTED_ADDON_TEMPLATES.map((template: AddonTemplate) => {
-          return (
-            <TemplateBlock
-              key={template.type}
-              onClick={() => {
-                history.push(`/addons/new?addon_name=${template.type}`);
-              }}
-            >
-              <Icon src={template.icon} />
-              <TemplateTitle>{template.displayName}</TemplateTitle>
-              <TemplateDescription>{template.description}</TemplateDescription>
-              <Spacer y={0.5} />
-              {template.tags.map((t) => (
-                <Tag
-                  bottom="10px"
-                  left="12px"
-                  style={{ background: AddonTemplateTagColor[t] }}
-                  key={t}
-                >
-                  {t}
-                </Tag>
-              ))}
-            </TemplateBlock>
-          );
-        })}
+        {SUPPORTED_ADDON_TEMPLATES.map(
+          (template: AddonTemplate<ClientAddonType>) => {
+            return (
+              <TemplateBlock
+                key={template.type}
+                onClick={() => {
+                  history.push(`/addons/new?addon_name=${template.type}`);
+                }}
+              >
+                <Icon src={template.icon} />
+                <TemplateTitle>{template.displayName}</TemplateTitle>
+                <TemplateDescription>
+                  {template.description}
+                </TemplateDescription>
+                <Spacer y={0.5} />
+                {template.tags.map((t) => (
+                  <Tag
+                    bottom="10px"
+                    left="12px"
+                    style={{ background: AddonTemplateTagColor[t] }}
+                    key={t}
+                  >
+                    {t}
+                  </Tag>
+                ))}
+              </TemplateBlock>
+            );
+          }
+        )}
       </TemplateListWrapper>
     </StyledTemplateComponent>
   );

+ 2 - 2
dashboard/src/main/home/add-on-dashboard/datadog/DatadogForm.tsx

@@ -39,10 +39,10 @@ const DatadogForm: React.FC = () => {
       <Text>DataDog API Key</Text>
       <Spacer y={0.5} />
       <ControlledInput
-        type="text"
+        type="password"
         width="300px"
         {...register("config.apiKey")}
-        placeholder=""
+        placeholder="my-api-key"
         error={errors.config?.apiKey?.message}
       />
       <Spacer y={1} />

+ 4 - 1
dashboard/src/main/home/add-on-dashboard/metabase/MetabaseForm.tsx

@@ -133,6 +133,7 @@ const MetabaseDatastoreConnection: React.FC = () => {
               type="text"
               width="600px"
               {...register("config.datastore.host")}
+              placeholder="my-host.com"
               error={errors.config?.datastore?.host?.message}
             />
           </td>
@@ -174,6 +175,7 @@ const MetabaseDatastoreConnection: React.FC = () => {
               type="text"
               width="600px"
               {...register("config.datastore.username")}
+              placeholder="my-username"
               error={errors.config?.datastore?.username?.message}
             />
           </td>
@@ -184,9 +186,10 @@ const MetabaseDatastoreConnection: React.FC = () => {
           </td>
           <td>
             <ControlledInput
-              type="text"
+              type="password"
               width="600px"
               {...register("config.datastore.password")}
+              placeholder="*****"
               error={errors.config?.datastore?.password?.message}
             />
           </td>

+ 2 - 2
dashboard/src/main/home/add-on-dashboard/mezmo/MezmoForm.tsx

@@ -28,10 +28,10 @@ const MezmoForm: React.FC = () => {
       <Text>Ingestion Key</Text>
       <Spacer y={0.5} />
       <ControlledInput
-        type="text"
+        type="password"
         width="300px"
         {...register("config.ingestionKey")}
-        placeholder="*****"
+        placeholder="my-ingestion-key"
         error={errors.config?.ingestionKey?.message}
       />
       <Spacer y={1} />

+ 3 - 3
dashboard/src/main/home/add-on-dashboard/newrelic/NewRelicForm.tsx

@@ -28,7 +28,7 @@ const NewRelicForm: React.FC = () => {
       <Text>NewRelic License Key</Text>
       <Spacer y={0.5} />
       <ControlledInput
-        type="text"
+        type="password"
         width="300px"
         {...register("config.licenseKey")}
         placeholder="*****"
@@ -38,7 +38,7 @@ const NewRelicForm: React.FC = () => {
       <Text>NewRelic Insights Key</Text>
       <Spacer y={0.5} />
       <ControlledInput
-        type="text"
+        type="password"
         width="300px"
         {...register("config.insightsKey")}
         placeholder="*****"
@@ -48,7 +48,7 @@ const NewRelicForm: React.FC = () => {
       <Text>NewRelic Personal API Key</Text>
       <Spacer y={0.5} />
       <ControlledInput
-        type="text"
+        type="password"
         width="300px"
         {...register("config.personalApiKey")}
         placeholder="*****"

+ 1 - 1
dashboard/src/main/home/add-on-dashboard/tailscale/TailscaleForm.tsx

@@ -47,7 +47,7 @@ const TailscaleForm: React.FC = () => {
       </Text>
       <Spacer y={0.5} />
       <ControlledInput
-        type="text"
+        type="password"
         width="500px"
         {...register("config.authKey")}
         placeholder="*****"