Просмотр исходного кода

support multiple custom domains (#3330)

Co-authored-by: sunguroku <65516095+sunguroku@users.noreply.github.com>
Feroze Mohideen 2 лет назад
Родитель
Сommit
13cc270346

+ 1 - 1
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx

@@ -61,7 +61,7 @@ const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData }) => {
       );
 
       setNumPages(res.data.num_pages);
-      setEvents(res.data.events);
+      setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)) ?? []);
     } catch (err) {
       setError(err);
     } finally {

+ 8 - 0
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/types.ts

@@ -27,4 +27,12 @@ export const PorterAppEvent = {
             metadata: data.metadata ?? {},
         };
     }
+}
+export interface PorterAppDeployEvent extends PorterAppEvent {
+    type: PorterAppEventType.DEPLOY;
+    metadata: {
+        image_tag: string;
+        revision: number;
+        service_status: Record<string, string>;
+    };
 }

+ 38 - 19
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -26,14 +26,20 @@ type ServiceBoolean = {
     readOnly: boolean;
     value: boolean;
 }
-export type ServiceArray<T extends ServiceString | ServiceBoolean> = {
+export type ServiceArray<T extends ServiceString | ServiceBoolean> = T[];
+const ServiceArray = {
+    serialize: <T extends ServiceString | ServiceBoolean>(serviceArray: ServiceArray<T>) => {
+        return serviceArray.map((service) => service.value).filter((val) => val !== '');
+    }
+}
+export type ServiceKeyValueArray<T extends ServiceString | ServiceBoolean> = {
     key: string;
     value: T;
 }[];
-const ServiceArray = {
-    serialize: <T extends ServiceString | ServiceBoolean>(serviceArray: ServiceArray<T>) => {
+const ServiceKeyValueArray = {
+    serialize: <T extends ServiceString | ServiceBoolean>(serviceKeyValueArray: ServiceKeyValueArray<T>) => {
         const map: Record<string, string> = {};
-        serviceArray.map(({ key, value }: {
+        serviceKeyValueArray.map(({ key, value }: {
             key: string;
             value: T;
         }) => {
@@ -47,10 +53,10 @@ const ServiceArray = {
 
 type Ingress = {
     enabled: ServiceBoolean;
-    customDomain: ServiceString;
-    hosts: ServiceString;
+    customDomains: ServiceArray<ServiceString>;
+    hosts: ServiceArray<ServiceString>;
     porterHosts: ServiceString;
-    annotations: ServiceArray<ServiceString>;
+    annotations: ServiceKeyValueArray<ServiceString>;
 }
 type Autoscaling = {
     enabled: ServiceBoolean,
@@ -103,7 +109,20 @@ const ServiceField = {
             value: overrideValue ?? defaultValue,
         }
     },
-    array: (defaultMap: Record<string, string>, overrideMap?: Record<string, string>): ServiceArray<ServiceString> => {
+    array: (defaultValues: string[], overrideValues?: string[]): ServiceArray<ServiceString> => {
+        const serviceMap: Record<string, ServiceString> = {};
+        for (const val of defaultValues) {
+            serviceMap[val] = ServiceField.string(val);
+        }
+        for (const val of overrideValues ?? []) {
+            serviceMap[val] = ServiceField.string('', val);
+        }
+        if (Object.keys(serviceMap).length == 0) {
+            return [];
+        }
+        return Object.values(serviceMap);
+    },
+    keyValueArray: (defaultMap: Record<string, string>, overrideMap?: Record<string, string>): ServiceKeyValueArray<ServiceString> => {
         const serviceMap: Record<string, ServiceString> = {};
         for (const key in defaultMap) {
             serviceMap[key] = ServiceField.string(defaultMap[key]);
@@ -156,10 +175,10 @@ const WebService = {
         },
         ingress: {
             enabled: ServiceField.boolean(true, porterJson?.apps?.[name]?.config?.ingress?.enabled),
-            customDomain: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
-            hosts: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
+            customDomains: ServiceField.array([], porterJson?.apps?.[name]?.config?.ingress?.hosts),
+            hosts: ServiceField.array([], porterJson?.apps?.[name]?.config?.ingress?.hosts),
             porterHosts: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.porter_hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.porter_hosts[0] : undefined),
-            annotations: ServiceField.array({}, porterJson?.apps?.[name]?.config?.ingress?.annotations)
+            annotations: ServiceField.keyValueArray({}, porterJson?.apps?.[name]?.config?.ingress?.annotations)
         },
         port: ServiceField.string('3000', porterJson?.apps?.[name]?.config?.container?.port),
         canDelete: porterJson?.apps?.[name] == null,
@@ -212,10 +231,10 @@ const WebService = {
             },
             ingress: {
                 enabled: service.ingress.enabled.value,
-                custom_domain: service.ingress.customDomain.value ? true : false,
-                hosts: service.ingress.customDomain.value ? [service.ingress.customDomain.value] : [],
+                custom_domain: service.ingress.customDomains.length ? true : false,
+                hosts: ServiceArray.serialize(service.ingress.customDomains),
                 porter_hosts: service.ingress.porterHosts.value ? [service.ingress.porterHosts.value] : [],
-                annotations: ServiceArray.serialize(service.ingress.annotations),
+                annotations: ServiceKeyValueArray.serialize(service.ingress.annotations),
             },
             service: {
                 port: service.port.value,
@@ -266,10 +285,10 @@ const WebService = {
             },
             ingress: {
                 enabled: ServiceField.boolean(values.ingress?.enabled ?? false, porterJson?.apps?.[name]?.config?.ingress?.enabled),
-                customDomain: ServiceField.string(values.ingress?.hosts?.length ? values.ingress.hosts[0] : '', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
-                hosts: ServiceField.string(values.ingress?.hosts?.length ? values.ingress.hosts[0] : '', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
+                customDomains: ServiceField.array(values.ingress?.hosts ?? [], porterJson?.apps?.[name]?.config?.ingress?.hosts),
+                hosts: ServiceField.array(values.ingress?.hosts ?? [], porterJson?.apps?.[name]?.config?.ingress?.hosts),
                 porterHosts: ServiceField.string(values.ingress?.porter_hosts?.length ? values.ingress.porter_hosts[0] : '', porterJson?.apps?.[name]?.config?.ingress?.porter_hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.porter_hosts[0] : undefined),
-                annotations: ServiceField.array(values.ingress?.annotations ?? {}, porterJson?.apps?.[name]?.config?.ingress?.annotations),
+                annotations: ServiceField.keyValueArray(values.ingress?.annotations ?? {}, porterJson?.apps?.[name]?.config?.ingress?.annotations),
             },
             port: ServiceField.string(values.container?.port ?? '', porterJson?.apps?.[name]?.config?.container?.port),
             canDelete: porterJson?.apps?.[name] == null,
@@ -634,8 +653,8 @@ export const Service = {
                 continue;
             }
             if (values.ingress.porter_hosts?.length > 0 || (values.ingress.custom_domain && values.ingress.hosts?.length > 0)) {
-                if (values.ingress.custom_domain && values.ingress.hosts?.length > 0) {
-                    // if they have a custom domain, use that
+                if (values.ingress.custom_domain && values.ingress.hosts?.length === 1) {
+                    // if they have a single custom domain, use that
                     matchedWebHost = values.ingress.hosts[0];
                 } else {
                     // otherwise, use their porter domain

+ 102 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/CustomDomains.tsx

@@ -0,0 +1,102 @@
+import React from 'react';
+import { ServiceArray, ServiceString } from '../serviceTypes';
+import Button from 'components/porter/Button';
+import styled from 'styled-components';
+import Input from 'components/porter/Input';
+import Spacer from 'components/porter/Spacer';
+
+interface Props {
+    customDomains: ServiceArray<ServiceString>;
+    onChange: (customDomains: ServiceArray<ServiceString>) => void;
+}
+
+const CustomDomains: React.FC<Props> = ({ customDomains, onChange }) => {
+    const renderInputs = () => {
+        return customDomains.map((customDomain, i) => {
+            return (
+                <>
+                    <AnnotationContainer key={i}>
+                        <Input
+                            placeholder="ex: my-app.my-domain.com"
+                            value={customDomain.value}
+                            setValue={(e) => {
+                                const newCustomDomains = [...customDomains];
+                                newCustomDomains[i] = { readOnly: false, value: e };
+                                onChange(newCustomDomains);
+                            }}
+                            disabled={customDomain.readOnly}
+                            width="275px"
+                            disabledTooltip={
+                                "You may only edit this field in your porter.yaml."
+                            }
+                        />
+                        <DeleteButton
+                            onClick={() => {
+                                //remove customDomain at the index
+                                const newCustomDomains = [...customDomains];
+                                newCustomDomains.splice(i, 1);
+                                onChange(newCustomDomains);
+                            }}
+                        >
+                            <i className="material-icons">cancel</i>
+                        </DeleteButton>
+                    </AnnotationContainer>
+                    <Spacer y={0.25} />
+                </>
+            );
+        });
+    };
+
+    return (
+        <CustomDomainsContainer>
+            {customDomains.length !== 0 &&
+                <>
+                    {renderInputs()}
+                    <Spacer y={0.5} />
+                </>
+            }
+            <Button
+                onClick={() => {
+                    const newCustomDomains = [...customDomains];
+                    newCustomDomains.push({ readOnly: false, value: "" });
+                    onChange(newCustomDomains);
+                }}
+            >
+                + Add Custom Domain
+            </Button>
+        </CustomDomainsContainer >
+    )
+};
+
+export default CustomDomains;
+
+const CustomDomainsContainer = styled.div`
+`;
+
+const AnnotationContainer = styled.div`
+    display: flex;
+    align-items: center;
+    gap: 5px;
+`
+
+const DeleteButton = styled.div`
+  width: 15px;
+  height: 15px;
+  display: flex;
+  align-items: center;
+  margin-left: 8px;
+  margin-top: -3px;
+  justify-content: center;
+
+  > i {
+    font-size: 17px;
+    color: #ffffff44;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    :hover {
+      color: #ffffff88;
+    }
+  }
+`;

+ 4 - 4
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/IngressCustomAnnotations.tsx

@@ -1,13 +1,13 @@
 import React from 'react';
-import { ServiceArray, ServiceString } from '../serviceTypes';
+import { ServiceKeyValueArray, ServiceString } from '../serviceTypes';
 import Button from 'components/porter/Button';
 import styled from 'styled-components';
 import Input from 'components/porter/Input';
 import Spacer from 'components/porter/Spacer';
 
 interface Props {
-    annotations: ServiceArray<ServiceString>;
-    onChange: (annotations: ServiceArray<ServiceString>) => void;
+    annotations: ServiceKeyValueArray<ServiceString>;
+    onChange: (annotations: ServiceKeyValueArray<ServiceString>) => void;
 }
 
 const IngressCustomAnnotations: React.FC<Props> = ({ annotations, onChange }) => {
@@ -76,7 +76,7 @@ const IngressCustomAnnotations: React.FC<Props> = ({ annotations, onChange }) =>
                     onChange(newAnnotations);
                 }}
             >
-                Add Annotation
+                + Add Annotation
             </Button>
         </IngressCustomAnnotationsContainer >
     )

+ 36 - 45
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WebTabs.tsx

@@ -9,6 +9,7 @@ import AnimateHeight, { Height } from "react-animate-height";
 import { Context } from "shared/Context";
 import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING } from "./utils";
 import IngressCustomAnnotations from "./IngressCustomAnnotations";
+import CustomDomains from "./CustomDomains";
 
 interface Props {
   service: WebService;
@@ -18,10 +19,10 @@ interface Props {
 
 
 const NETWORKING_HEIGHT_WITHOUT_INGRESS = 204;
-const NETWORKING_HEIGHT_WITH_INGRESS = 425;
+const NETWORKING_HEIGHT_WITH_INGRESS = 395;
 const ADVANCED_BASE_HEIGHT = 215;
 const PROBE_INPUTS_HEIGHT = 230;
-const CUSTOM_ANNOTATION_HEIGHT = 53;
+const CUSTOM_ANNOTATION_HEIGHT = 44;
 
 const WebTabs: React.FC<Props> = ({
   service,
@@ -104,39 +105,27 @@ const WebTabs: React.FC<Props> = ({
           <Text color="helper">Expose to external traffic</Text>
         </Checkbox>
         <AnimateHeight height={service.ingress.enabled.value ? 'auto' : 0}>
-          <Spacer y={1} />
-          <Input
-            label={
-              <>
-                <span>Custom domain</span>
-                <a
-                  href="https://docs.porter.run/standard/deploying-applications/https-and-domains/custom-domains"
-                  target="_blank"
-                >
-                  &nbsp;(?)
-                </a>
-              </>
-            }
-            placeholder="ex: my-app.my-domain.com"
-            value={service.ingress.customDomain.value}
-            disabled={service.ingress.customDomain.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                ingress: {
-                  ...service.ingress,
-                  customDomain: { readOnly: false, value: e },
-                },
-              });
+          <Spacer y={0.5} />
+          {getApplicationURLText()}
+          <Spacer y={0.5} />
+          <Text color="helper">
+            Custom domains
+            <a
+              href="https://docs.porter.run/standard/deploying-applications/https-and-domains/custom-domains"
+              target="_blank"
+            >
+              &nbsp;(?)
+            </a>
+          </Text>
+          <Spacer y={0.5} />
+          <CustomDomains
+            customDomains={service.ingress.customDomains}
+            onChange={(customDomains) => {
+              editService({ ...service, ingress: { ...service.ingress, customDomains: customDomains } });
+              setHeight(calculateNetworkingHeight());
             }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
           />
-          <Spacer y={1} />
-          {getApplicationURLText()}
-          <Spacer y={1} />
+          <Spacer y={0.5} />
           <Text color="helper">
             Ingress Custom Annotations
             <a
@@ -443,7 +432,7 @@ const WebTabs: React.FC<Props> = ({
   };
 
   const calculateNetworkingHeight = () => {
-    return NETWORKING_HEIGHT_WITH_INGRESS + (service.ingress.annotations.length * CUSTOM_ANNOTATION_HEIGHT);
+    return NETWORKING_HEIGHT_WITH_INGRESS + (service.ingress.annotations.length * CUSTOM_ANNOTATION_HEIGHT) + (service.ingress.customDomains.length * CUSTOM_ANNOTATION_HEIGHT);
   }
 
   const renderAdvanced = () => {
@@ -788,12 +777,17 @@ const WebTabs: React.FC<Props> = ({
   };
 
   const getApplicationURLText = () => {
-    if (service.ingress.hosts.value !== "") {
+    if (service.ingress.hosts.length !== 0) {
       return (
-        <Text>Application URL:{" "}
-          <a href={Service.prefixSubdomain(service.ingress.hosts.value)} target="_blank">
-            {service.ingress.hosts.value}
-          </a>
+        <Text>{`Application URL${service.ingress.hosts.length === 1 ? "" : "s"}: `}
+          {service.ingress.hosts.map((host, i) => {
+            return (
+              <a href={Service.prefixSubdomain(host.value)} target="_blank">
+                {host.value}
+                {i !== service.ingress.hosts.length - 1 && ", "}
+              </a>
+            )
+          })}
         </Text>
       )
     } else if (service.ingress.porterHosts.value !== "") {
@@ -804,13 +798,10 @@ const WebTabs: React.FC<Props> = ({
           </a>
         </Text>
       )
-    } else if (service.ingress.customDomain.value !== "") {
+    } else if (service.ingress.customDomains.length !== 0) {
       return (
-        <Text color="helper">Application URL: Your application will be available at{" "}
-          <a href={Service.prefixSubdomain(service.ingress.customDomain.value)} target="_blank">
-            {service.ingress.customDomain.value}
-          </a>
-          {" "}on next deploy.
+        <Text color="helper">
+          {`Application URL${service.ingress.customDomains.length === 1 ? "" : "s"}: Your application will be available at the specified custom domain${service.ingress.customDomains.length === 1 ? "" : "s"} on next deploy.`}
         </Text>
       )
     } else {