diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLIngressHelper.test.ts b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLIngressHelper.test.ts new file mode 100644 index 00000000000..30caac19a79 --- /dev/null +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLIngressHelper.test.ts @@ -0,0 +1,76 @@ +import { IHTTPIngressPath, IIngressRule, IIngressSpec, IResource } from "shared/types"; +import { GetURLItemFromIngress } from "./AccessURLIngressHelper"; + +describe("GetURLItemFromIngress", () => { + interface Itest { + description: string; + hosts: string[]; + paths: IHTTPIngressPath[]; + tlsHosts: string[]; + expectedURLs: string[]; + } + const tests: Itest[] = [ + { + description: "it should show the host without port", + hosts: ["foo.bar"], + paths: [], + tlsHosts: [], + expectedURLs: ["http://foo.bar"], + }, + { + description: "it should show several hosts without port", + hosts: ["foo.bar", "not-foo.bar"], + paths: [], + tlsHosts: [], + expectedURLs: ["http://foo.bar", "http://not-foo.bar"], + }, + { + description: "it should show the host with the different paths", + hosts: ["foo.bar"], + paths: [{ path: "/one" }, { path: "/two" }], + tlsHosts: [], + expectedURLs: ["http://foo.bar/one", "http://foo.bar/two"], + }, + { + description: "it should show TLS hosts with https", + hosts: ["foo.bar", "not-foo.bar"], + paths: [], + tlsHosts: ["foo.bar"], + expectedURLs: ["https://foo.bar", "http://not-foo.bar"], + }, + ]; + tests.forEach(test => { + it(test.description, () => { + const ingress = { + metadata: { + name: "foo", + }, + spec: { + rules: [], + } as IIngressSpec, + } as IResource; + test.hosts.forEach(h => { + const rule = { + host: h, + http: { + paths: [], + }, + } as IIngressRule; + if (test.paths.length > 0) { + rule.http.paths = test.paths; + } + ingress.spec.rules.push(rule); + }); + if (test.tlsHosts.length > 0) { + ingress.spec.tls = [ + { + hosts: test.tlsHosts, + }, + ]; + } + const ingressItem = GetURLItemFromIngress(ingress); + expect(ingressItem.isLink).toBe(true); + expect(ingressItem.URLs).toEqual(test.expectedURLs); + }); + }); +}); diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLIngressHelper.tsx b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLIngressHelper.tsx new file mode 100644 index 00000000000..393afc0dce5 --- /dev/null +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLIngressHelper.tsx @@ -0,0 +1,40 @@ +import { IIngressSpec, IIngressTLS, IResource } from "shared/types"; +import { IURLItem } from "./IURLItem"; + +// URLs returns the list of URLs obtained from the service status +function URLs(ingress: IResource): string[] { + const spec = ingress.spec as IIngressSpec; + const res: string[] = []; + spec.rules.forEach(r => { + if (r.http.paths.length > 0) { + r.http.paths.forEach(p => { + res.push(getURL(r.host, spec.tls, p.path)); + }); + } else { + res.push(getURL(r.host, spec.tls)); + } + }); + return res; +} + +// getURL returns a full URL based on a hostname, a TLS configuration and a optional path +function getURL(hostname: string, tls?: IIngressTLS[], path?: string) { + // If the hostname is configured within the TLS hosts it will use HTTPS + const protocol = + tls && + tls.some(tlsRule => { + return tlsRule.hosts.indexOf(hostname) > -1; + }) + ? "https" + : "http"; + return `${protocol}://${hostname}${path || ""}`; +} + +export function GetURLItemFromIngress(ingress: IResource) { + return { + name: ingress.metadata.name, + type: "Ingress", + isLink: true, + URLs: URLs(ingress), + } as IURLItem; +} diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLItem.test.tsx b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLItem.test.tsx index cec0ab8782a..c14013b0c8c 100644 --- a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLItem.test.tsx +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLItem.test.tsx @@ -1,111 +1,28 @@ import { shallow } from "enzyme"; -import context from "jest-plugin-context"; import * as React from "react"; -import { IResource, IServiceSpec, IServiceStatus } from "shared/types"; import AccessURLItem from "./AccessURLItem"; +import { IURLItem } from "./IURLItem"; -context("when the status is empty", () => { - const service = { - metadata: { - name: "foo", - }, - spec: { - type: "LoadBalancer", - ports: [{ port: 8080 }], - } as IServiceSpec, - status: { - loadBalancer: {}, - } as IServiceStatus, - } as IResource; - - it("should show a Pending text", () => { - const wrapper = shallow(); - expect(wrapper.text()).toContain("Pending"); - expect(wrapper).toMatchSnapshot(); - }); - - it("should not include a link", () => { - const wrapper = shallow(); - expect(wrapper.find(".ServiceItem")).toExist(); - const link = wrapper.find(".ServiceItem").find("a"); - expect(link).not.toExist(); - }); +it("should show only a message (without a link) if the item is not a link", () => { + const item = { name: "foo", isLink: false, URLs: ["Pending"] } as IURLItem; + const wrapper = shallow(); + expect(wrapper.text()).toContain("Pending"); + expect(wrapper).toMatchSnapshot(); + const link = wrapper.find(".ServiceItem").find("a"); + expect(link).not.toExist(); }); -context("when the status is populated", () => { - interface Itest { - description: string; - ports: any[]; - ingress: any[]; - expectedURLs: string[]; - } - const tests: Itest[] = [ - { - description: "it should show the IP and port if it's not known", - ports: [{ port: 8080 }], - ingress: [{ ip: "1.2.3.4" }], - expectedURLs: ["http://1.2.3.4:8080"], - }, - { - description: "it should show the hostname and port if it's not known", - ports: [{ port: 8080 }], - ingress: [{ hostname: "1.2.3.4" }], - expectedURLs: ["http://1.2.3.4:8080"], - }, - { - description: "it should show the IP and skip the port if it's known", - ports: [{ port: 80 }], - ingress: [{ ip: "1.2.3.4" }], - expectedURLs: ["http://1.2.3.4"], - }, - { - description: "it should show the https URL if the port is 443", - ports: [{ port: 443 }], - ingress: [{ ip: "1.2.3.4" }], - expectedURLs: ["https://1.2.3.4"], - }, - { - description: "it should show several URLs if there are multipe ports", - ports: [{ port: 8080 }, { port: 8081 }], - ingress: [{ ip: "1.2.3.4" }], - expectedURLs: ["http://1.2.3.4:8080", "http://1.2.3.4:8081"], - }, - { - description: "it should show several URLs if there are ingress ports", - ports: [{ port: 8080 }, { port: 8081 }], - ingress: [{ ip: "1.2.3.4" }, { hostname: "foo.bar" }], - expectedURLs: [ - "http://1.2.3.4:8080", - "http://1.2.3.4:8081", - "http://foo.bar:8080", - "http://foo.bar:8081", - ], - }, - ]; - tests.forEach(test => { - it(test.description, () => { - const service = { - metadata: { - name: "foo", - }, - spec: { - type: "LoadBalancer", - ports: test.ports, - }, - status: { - loadBalancer: { - ingress: test.ingress, - }, - } as IServiceStatus, - } as IResource; - const wrapper = shallow(); - test.expectedURLs.forEach(url => { - expect(wrapper.find(".ServiceItem")).toExist(); - const link = wrapper.find(".ServiceItem").find("a"); - expect(link).toExist(); - expect(wrapper.text()).toContain(url); - }); - }); - }); +it("should show only an URL with a link", () => { + const item = { + name: "foo", + isLink: true, + URLs: ["http://1.2.3.4:8080", "https://foo.bar"], + } as IURLItem; + const wrapper = shallow(); + expect(wrapper.text()).toContain("http://1.2.3.4:8080"); + expect(wrapper.text()).toContain("https://foo.bar"); + expect(wrapper).toMatchSnapshot(); + const link = wrapper.find(".ServiceItem").find("a"); + expect(link).toExist(); }); diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLItem.tsx b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLItem.tsx index 1990a896be2..95a3e0e6480 100644 --- a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLItem.tsx +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLItem.tsx @@ -1,82 +1,38 @@ import * as React from "react"; -import { IResource, IServiceSpec, IServiceStatus } from "../../../../shared/types"; import "./AccessURLItem.css"; +import { IURLItem } from "./IURLItem"; interface IAccessURLItem { - loadBalancerService: IResource; + URLItem: IURLItem; } -class AccessURLItem extends React.Component { - public render() { - const { loadBalancerService } = this.props; - const isLink = this.isLink(); - return ( - - {loadBalancerService.metadata.name} - {loadBalancerService.spec.type} - - {this.URLs().map(l => ( - - {isLink ? ( - - {l} - - ) : ( - l - )} - - ))} - - - ); - } - - // isLink returns true if there are any link in the Item - private isLink(): boolean { - if ( - this.props.loadBalancerService.status.loadBalancer.ingress && - this.props.loadBalancerService.status.loadBalancer.ingress.length - ) { - return true; - } - return false; - } - - // URLs returns the list of URLs obtained from the service status - private URLs(): string[] { - const URLs: string[] = []; - const { loadBalancerService } = this.props; - const status: IServiceStatus = loadBalancerService.status; - if (status.loadBalancer.ingress && status.loadBalancer.ingress.length) { - status.loadBalancer.ingress.forEach(i => { - (loadBalancerService.spec as IServiceSpec).ports.forEach(port => { - if (i.hostname) { - URLs.push(this.getURL(i.hostname, port.port)); - } - if (i.ip) { - URLs.push(this.getURL(i.ip, port.port)); - } - }); - }); - } else { - URLs.push("Pending"); - } - return URLs; - } - - // getURL returns a full URL adding the protocol and the port if needed - private getURL(base: string, port: number) { - const protocol = port === 443 ? "https" : "http"; - // Only show the port in the URL if it's not a standard HTTP/HTTPS port - const portSuffix = port === 443 || port === 80 ? "" : `:${port}`; - return `${protocol}://${base}${portSuffix}`; - } -} +const AccessURLItem: React.SFC = props => { + const { URLItem } = props; + return ( + + {URLItem.name} + {URLItem.type} + + {URLItem.URLs.map(l => ( + + {URLItem.isLink ? ( + + {l} + + ) : ( + l + )} + + ))} + + + ); +}; export default AccessURLItem; diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLServiceHelper.test.ts b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLServiceHelper.test.ts new file mode 100644 index 00000000000..1d1aebdaeee --- /dev/null +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLServiceHelper.test.ts @@ -0,0 +1,89 @@ +import { IResource, IServiceStatus } from "shared/types"; +import { GetURLItemFromService } from "./AccessURLServiceHelper"; + +describe("GetURLItemFromService", () => { + interface Itest { + description: string; + ports: any[]; + ingress: any[]; + expectedLink: boolean; + expectedURLs: string[]; + } + const tests: Itest[] = [ + { + description: "it not return a link if the ingress definition is empty", + ports: [{ port: 8080 }], + ingress: [], + expectedLink: false, + expectedURLs: ["Pending"], + }, + { + description: "it should show the IP and port if it's not known", + ports: [{ port: 8080 }], + ingress: [{ ip: "1.2.3.4" }], + expectedLink: true, + expectedURLs: ["http://1.2.3.4:8080"], + }, + { + description: "it should show the hostname and port if it's not known", + ports: [{ port: 8080 }], + ingress: [{ hostname: "1.2.3.4" }], + expectedLink: true, + expectedURLs: ["http://1.2.3.4:8080"], + }, + { + description: "it should show the IP and skip the port if it's known", + ports: [{ port: 80 }], + ingress: [{ ip: "1.2.3.4" }], + expectedLink: true, + expectedURLs: ["http://1.2.3.4"], + }, + { + description: "it should show the https URL if the port is 443", + ports: [{ port: 443 }], + ingress: [{ ip: "1.2.3.4" }], + expectedLink: true, + expectedURLs: ["https://1.2.3.4"], + }, + { + description: "it should show several URLs if there are multipe ports", + ports: [{ port: 8080 }, { port: 8081 }], + ingress: [{ ip: "1.2.3.4" }], + expectedLink: true, + expectedURLs: ["http://1.2.3.4:8080", "http://1.2.3.4:8081"], + }, + { + description: "it should show several URLs if there are ingress ports", + ports: [{ port: 8080 }, { port: 8081 }], + ingress: [{ ip: "1.2.3.4" }, { hostname: "foo.bar" }], + expectedLink: true, + expectedURLs: [ + "http://1.2.3.4:8080", + "http://1.2.3.4:8081", + "http://foo.bar:8080", + "http://foo.bar:8081", + ], + }, + ]; + tests.forEach(test => { + it(test.description, () => { + const service = { + metadata: { + name: "foo", + }, + spec: { + type: "LoadBalancer", + ports: test.ports, + }, + status: { + loadBalancer: { + ingress: test.ingress, + }, + } as IServiceStatus, + } as IResource; + const svcItem = GetURLItemFromService(service); + expect(test.expectedLink).toEqual(svcItem.isLink); + expect(test.expectedURLs).toEqual(svcItem.URLs); + }); + }); +}); diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLServiceHelper.ts b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLServiceHelper.ts new file mode 100644 index 00000000000..9c4f1547baf --- /dev/null +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/AccessURLServiceHelper.ts @@ -0,0 +1,51 @@ +import { IResource, IServiceSpec, IServiceStatus } from "shared/types"; +import { IURLItem } from "./IURLItem"; + +// isLink returns true if there are any link in the Item +function isLink(loadBalancerService: IResource): boolean { + if ( + loadBalancerService.status.loadBalancer.ingress && + loadBalancerService.status.loadBalancer.ingress.length + ) { + return true; + } + return false; +} + +// URLs returns the list of URLs obtained from the service status +function URLs(loadBalancerService: IResource): string[] { + const res: string[] = []; + const status: IServiceStatus = loadBalancerService.status; + if (status.loadBalancer.ingress && status.loadBalancer.ingress.length) { + status.loadBalancer.ingress.forEach(i => { + (loadBalancerService.spec as IServiceSpec).ports.forEach(port => { + if (i.hostname) { + res.push(getURL(i.hostname, port.port)); + } + if (i.ip) { + res.push(getURL(i.ip, port.port)); + } + }); + }); + } else { + res.push("Pending"); + } + return res; +} + +// getURL returns a full URL adding the protocol and the port if needed +function getURL(base: string, port: number) { + const protocol = port === 443 ? "https" : "http"; + // Only show the port in the URL if it's not a standard HTTP/HTTPS port + const portSuffix = port === 443 || port === 80 ? "" : `:${port}`; + return `${protocol}://${base}${portSuffix}`; +} + +export function GetURLItemFromService(loadBalancerService: IResource) { + return { + name: loadBalancerService.metadata.name, + type: "Service LoadBalancer", + isLink: isLink(loadBalancerService), + URLs: URLs(loadBalancerService), + } as IURLItem; +} diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/IURLItem.tsx b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/IURLItem.tsx new file mode 100644 index 00000000000..d9a0c866676 --- /dev/null +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/IURLItem.tsx @@ -0,0 +1,6 @@ +export interface IURLItem { + name: string; + type: string; + isLink: boolean; + URLs: string[]; +} diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/__snapshots__/AccessURLItem.test.tsx.snap b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/__snapshots__/AccessURLItem.test.tsx.snap index 69dc8efe68e..4f655af5fdd 100644 --- a/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/__snapshots__/AccessURLItem.test.tsx.snap +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLItem/__snapshots__/AccessURLItem.test.tsx.snap @@ -1,13 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`when the status is empty should show a Pending text 1`] = ` +exports[`should show only a message (without a link) if the item is not a link 1`] = ` foo - - LoadBalancer - + `; + +exports[`should show only an URL with a link 1`] = ` + + + foo + + + + + + http://1.2.3.4:8080 + + + + + https://foo.bar + + + + +`; diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLTable.test.tsx b/dashboard/src/components/AppView/AccessURLTable/AccessURLTable.test.tsx index 57d99e44c27..73b43c7e108 100644 --- a/dashboard/src/components/AppView/AccessURLTable/AccessURLTable.test.tsx +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLTable.test.tsx @@ -1,45 +1,112 @@ import { shallow } from "enzyme"; +import context from "jest-plugin-context"; import * as React from "react"; -import { IResource, IServiceSpec, IServiceStatus } from "shared/types"; +import { IIngressSpec, IResource, IServiceSpec, IServiceStatus } from "shared/types"; import AccessURLItem from "./AccessURLItem"; import AccessURLTable from "./AccessURLTable"; -it("should omit the Service Table if there are no public services", () => { - const service = { - metadata: { - name: "foo", - }, - spec: { - type: "ClusterIP", - ports: [{ port: 8080 }], - } as IServiceSpec, - status: { - loadBalancer: {}, - } as IServiceStatus, - } as IResource; - const services = {}; - services[service.metadata.name] = service; - const wrapper = shallow(); - expect(wrapper.text()).toBe(""); +context("when the app contain services", () => { + it("should omit the Service Table if there are no public services", () => { + const service = { + metadata: { + name: "foo", + }, + spec: { + type: "ClusterIP", + ports: [{ port: 8080 }], + } as IServiceSpec, + status: { + loadBalancer: {}, + } as IServiceStatus, + } as IResource; + const services = {}; + services[service.metadata.name] = service; + const wrapper = shallow(); + expect(wrapper.text()).toBe(""); + }); + + it("should show the table if any service is a LoadBalancer", () => { + const service = { + metadata: { + name: "foo", + }, + spec: { + type: "LoadBalancer", + ports: [{ port: 8080 }], + } as IServiceSpec, + status: { + loadBalancer: {}, + } as IServiceStatus, + } as IResource; + const services = {}; + services[service.metadata.name] = service; + const wrapper = shallow(); + expect(wrapper.find(AccessURLItem)).toExist(); + expect(wrapper).toMatchSnapshot(); + }); +}); + +context("when the app contain ingresses", () => { + it("should show the table with available ingresses", () => { + const ingress = { + metadata: { + name: "foo", + }, + spec: { + rules: [ + { + host: "foo.bar", + http: { + paths: [{ path: "/ready" }], + }, + }, + ], + } as IIngressSpec, + }; + const ingresses = {}; + ingresses[ingress.metadata.name] = ingress; + const wrapper = shallow(); + expect(wrapper.find(AccessURLItem)).toExist(); + expect(wrapper).toMatchSnapshot(); + }); }); -it("should show the table if any service is a LoadBalancer", () => { - const service = { - metadata: { - name: "foo", - }, - spec: { - type: "LoadBalancer", - ports: [{ port: 8080 }], - } as IServiceSpec, - status: { - loadBalancer: {}, - } as IServiceStatus, - } as IResource; - const services = {}; - services[service.metadata.name] = service; - const wrapper = shallow(); - expect(wrapper.find(AccessURLItem)).toExist(); - expect(wrapper).toMatchSnapshot(); +context("when the app contain services and ingresses", () => { + it("should show the table with available svcs and ingresses", () => { + const service = { + metadata: { + name: "foo", + }, + spec: { + type: "LoadBalancer", + ports: [{ port: 8080 }], + } as IServiceSpec, + status: { + loadBalancer: {}, + } as IServiceStatus, + } as IResource; + const services = {}; + services[service.metadata.name] = service; + const ingress = { + metadata: { + name: "foo", + }, + spec: { + rules: [ + { + host: "foo.bar", + http: { + paths: [{ path: "/ready" }], + }, + }, + ], + } as IIngressSpec, + }; + const ingresses = {}; + ingresses[ingress.metadata.name] = ingress; + const wrapper = shallow(); + expect(wrapper.find(AccessURLItem)).toExist(); + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/dashboard/src/components/AppView/AccessURLTable/AccessURLTable.tsx b/dashboard/src/components/AppView/AccessURLTable/AccessURLTable.tsx index e82043bb0f2..32686aed65d 100644 --- a/dashboard/src/components/AppView/AccessURLTable/AccessURLTable.tsx +++ b/dashboard/src/components/AppView/AccessURLTable/AccessURLTable.tsx @@ -2,17 +2,20 @@ import * as React from "react"; import { IResource, IServiceSpec } from "../../../shared/types"; import AccessURLItem from "./AccessURLItem"; +import { GetURLItemFromIngress } from "./AccessURLItem/AccessURLIngressHelper"; +import { GetURLItemFromService } from "./AccessURLItem/AccessURLServiceHelper"; interface IServiceTableProps { services: { [s: string]: IResource }; + ingresses: { [i: string]: IResource }; } class AccessURLTable extends React.Component { public render() { - const { services } = this.props; + const { services, ingresses } = this.props; const publicServices = this.publicServices(); return ( - publicServices.length > 0 && ( + (publicServices.length > 0 || Object.keys(ingresses).length > 0) && ( @@ -22,8 +25,11 @@ class AccessURLTable extends React.Component { + {Object.keys(ingresses).map((k: string) => ( + + ))} {publicServices.map((k: string) => ( - + ))}
diff --git a/dashboard/src/components/AppView/AccessURLTable/__snapshots__/AccessURLTable.test.tsx.snap b/dashboard/src/components/AppView/AccessURLTable/__snapshots__/AccessURLTable.test.tsx.snap index 6565a48fd56..88218ad2129 100644 --- a/dashboard/src/components/AppView/AccessURLTable/__snapshots__/AccessURLTable.test.tsx.snap +++ b/dashboard/src/components/AppView/AccessURLTable/__snapshots__/AccessURLTable.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should show the table if any service is a LoadBalancer 1`] = ` +exports[`when the app contain ingresses should show the table with available ingresses 1`] = ` @@ -17,25 +17,96 @@ exports[`should show the table if any service is a LoadBalancer 1`] = ` + +
+`; + +exports[`when the app contain services and ingresses should show the table with available svcs and ingresses 1`] = ` + + + + + + + + + + + + +
+ NAME + + TYPE + + URL +
+`; + +exports[`when the app contain services should show the table if any service is a LoadBalancer 1`] = ` + + + + + + + + + +
+ NAME + + TYPE + + URL +
diff --git a/dashboard/src/components/AppView/AppView.test.tsx b/dashboard/src/components/AppView/AppView.test.tsx index 15fbd4916f4..a26a146f2cd 100644 --- a/dashboard/src/components/AppView/AppView.test.tsx +++ b/dashboard/src/components/AppView/AppView.test.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import { hapi } from "../../shared/hapi/release"; import itBehavesLike from "../../shared/specs"; -import { ForbiddenError, IResource, NotFoundError } from "../../shared/types"; +import { ForbiddenError, IIngressSpec, IResource, NotFoundError } from "../../shared/types"; import DeploymentStatus from "../DeploymentStatus"; import { ErrorSelector } from "../ErrorAlert"; import PermissionsErrorPage from "../ErrorAlert/PermissionsErrorAlert"; @@ -64,11 +64,16 @@ describe("AppViewComponent", () => { const resources = { configMap: { apiVersion: "v1", kind: "ConfigMap", metadata: { name: "cm-one" } }, deployment: { - apiVersion: "extensions/v1beta1", + apiVersion: "apps/v1beta1", kind: "Deployment", metadata: { name: "deployment-one" }, }, service: { apiVersion: "v1", kind: "Service", metadata: { name: "svc-one" } }, + ingress: { + apiVersion: "extensions/v1beta1", + kind: "Ingress", + metadata: { name: "ingress-one" }, + }, }; /* @@ -80,6 +85,7 @@ describe("AppViewComponent", () => { resources.deployment, resources.service, resources.configMap, + resources.ingress, ]); const wrapper = shallow(); @@ -88,13 +94,16 @@ describe("AppViewComponent", () => { wrapper.setProps(validProps); const sockets: WebSocket[] = wrapper.state("sockets"); - expect(sockets.length).toEqual(2); + expect(sockets.length).toEqual(3); expect(sockets[0].url).toBe( "ws://localhost/api/kube/apis/apps/v1beta1/namespaces/weee/deployments?watch=true&fieldSelector=metadata.name%3Ddeployment-one", ); expect(sockets[1].url).toBe( "ws://localhost/api/kube/api/v1/namespaces/weee/services?watch=true&fieldSelector=metadata.name%3Dsvc-one", ); + expect(sockets[2].url).toBe( + "ws://localhost/api/kube/apis/extensions/v1beta1/namespaces/weee/ingresses?watch=true&fieldSelector=metadata.name%3Dingress-one", + ); }); it("stores other k8s resources directly in the state", () => { @@ -188,5 +197,31 @@ describe("AppViewComponent", () => { expect(err.exists()).toBe(true); expect(err.html()).toContain("Application mr-sunshine not found"); }); + + it("renders an URL table if an Ingress exists", () => { + const wrapper = mount(); + const ingress = { + metadata: { + name: "foo", + }, + spec: { + rules: [ + { + host: "foo.bar", + http: { + paths: [{ path: "/ready" }], + }, + }, + ], + } as IIngressSpec, + } as IResource; + const ingresses = {}; + ingresses[ingress.metadata.name] = ingress; + wrapper.setState({ ingresses }); + const urlTable = wrapper.find(AccessURLTable); + expect(urlTable).toExist(); + expect(urlTable.text()).toContain("Ingress"); + expect(urlTable.text()).toContain("http://foo.bar/ready"); + }); }); }); diff --git a/dashboard/src/components/AppView/AppView.tsx b/dashboard/src/components/AppView/AppView.tsx index d74438d42df..00e784bca27 100644 --- a/dashboard/src/components/AppView/AppView.tsx +++ b/dashboard/src/components/AppView/AppView.tsx @@ -30,6 +30,7 @@ interface IAppViewState { deployments: { [d: string]: IResource }; otherResources: { [r: string]: IResource }; services: { [s: string]: IResource }; + ingresses: { [i: string]: IResource }; sockets: WebSocket[]; } @@ -51,6 +52,7 @@ const RequiredRBACRoles: { [s: string]: IRBACRole[] } = { class AppView extends React.Component { public state: IAppViewState = { deployments: {}, + ingresses: {}, otherResources: {}, services: {}, sockets: [], @@ -96,27 +98,16 @@ class AppView extends React.Component { const deployments = manifest.filter(d => d.kind === "Deployment"); const services = manifest.filter(d => d.kind === "Service"); - const apiBase = WebSocketHelper.apiBase(); + const ingresses = manifest.filter(d => d.kind === "Ingress"); const sockets: WebSocket[] = []; for (const d of deployments) { - const s = new WebSocket( - `${apiBase}/apis/apps/v1beta1/namespaces/${ - newApp.namespace - }/deployments?watch=true&fieldSelector=metadata.name%3D${d.metadata.name}`, - Auth.wsProtocols(), - ); - s.addEventListener("message", e => this.handleEvent(e)); - sockets.push(s); + sockets.push(this.getSocket("deployments", d.apiVersion, d.metadata.name, newApp.namespace)); } for (const svc of services) { - const s = new WebSocket( - `${apiBase}/api/v1/namespaces/${ - newApp.namespace - }/services?watch=true&fieldSelector=metadata.name%3D${svc.metadata.name}`, - Auth.wsProtocols(), - ); - s.addEventListener("message", e => this.handleEvent(e)); - sockets.push(s); + sockets.push(this.getSocket("services", svc.apiVersion, svc.metadata.name, newApp.namespace)); + } + for (const i of ingresses) { + sockets.push(this.getSocket("ingresses", i.apiVersion, i.metadata.name, newApp.namespace)); } this.setState({ sockets, @@ -138,6 +129,9 @@ class AppView extends React.Component { case "Service": this.setState({ services: { ...this.state.services, [key]: resource } }); break; + case "Ingress": + this.setState({ ingresses: { ...this.state.ingresses, [key]: resource } }); + break; } } @@ -193,8 +187,9 @@ class AppView extends React.Component { - {Object.keys(this.state.services).length > 0 && ( - + {(Object.keys(this.state.services).length > 0 || + Object.keys(this.state.ingresses).length > 0) && ( + )} { ); } + private getSocket( + resource: string, + apiVersion: string, + name: string, + namespace: string, + ): WebSocket { + const apiBase = WebSocketHelper.apiBase(); + const s = new WebSocket( + `${apiBase}/${ + apiVersion === "v1" ? "api/v1" : `apis/${apiVersion}` + }/namespaces/${namespace}/${resource}?watch=true&fieldSelector=metadata.name%3D${name}`, + Auth.wsProtocols(), + ); + s.addEventListener("message", e => this.handleEvent(e)); + return s; + } + private closeSockets() { const { sockets } = this.state; for (const s of sockets) { diff --git a/dashboard/src/shared/types.ts b/dashboard/src/shared/types.ts index 1d02a4649ec..6ba1c7d3481 100644 --- a/dashboard/src/shared/types.ts +++ b/dashboard/src/shared/types.ts @@ -111,6 +111,26 @@ export interface IPort { nodePort: string; } +export interface IHTTPIngressPath { + path: string; +} +export interface IIngressHTTP { + paths: IHTTPIngressPath[]; +} +export interface IIngressRule { + host: string; + http: IIngressHTTP; +} + +export interface IIngressTLS { + hosts: string[]; +} + +export interface IIngressSpec { + rules: IIngressRule[]; + tls?: IIngressTLS[]; +} + export interface IResource { apiVersion: string; kind: string;