From f12b2b6a99b55c2f60883b6a10ae0dcb65598a85 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Mon, 13 Apr 2020 18:20:19 +0300 Subject: [PATCH] Implement endpoints section (#246) Signed-off-by: Lauri Nevala --- .../client/api/endpoints/endpoint.api.ts | 122 +++++++++++++ dashboard/client/api/endpoints/index.ts | 1 + .../+network-endpoints/endpoint-details.scss | 2 + .../+network-endpoints/endpoint-details.tsx | 44 +++++ .../endpoint-subset-list.scss | 7 + .../endpoint-subset-list.tsx | 164 ++++++++++++++++++ .../+network-endpoints/endpoints.route.ts | 11 ++ .../+network-endpoints/endpoints.scss | 7 + .../+network-endpoints/endpoints.store.ts | 12 ++ .../+network-endpoints/endpoints.tsx | 72 ++++++++ .../components/+network-endpoints/index.ts | 3 + .../service-details-endpoint.tsx | 49 ++++++ .../+network-services/service-details.scss | 7 + .../+network-services/service-details.tsx | 15 +- .../client/components/+network/network.tsx | 7 + 15 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 dashboard/client/api/endpoints/endpoint.api.ts create mode 100644 dashboard/client/components/+network-endpoints/endpoint-details.scss create mode 100644 dashboard/client/components/+network-endpoints/endpoint-details.tsx create mode 100644 dashboard/client/components/+network-endpoints/endpoint-subset-list.scss create mode 100644 dashboard/client/components/+network-endpoints/endpoint-subset-list.tsx create mode 100644 dashboard/client/components/+network-endpoints/endpoints.route.ts create mode 100644 dashboard/client/components/+network-endpoints/endpoints.scss create mode 100644 dashboard/client/components/+network-endpoints/endpoints.store.ts create mode 100644 dashboard/client/components/+network-endpoints/endpoints.tsx create mode 100644 dashboard/client/components/+network-endpoints/index.ts create mode 100644 dashboard/client/components/+network-services/service-details-endpoint.tsx diff --git a/dashboard/client/api/endpoints/endpoint.api.ts b/dashboard/client/api/endpoints/endpoint.api.ts new file mode 100644 index 0000000000..e01c86072a --- /dev/null +++ b/dashboard/client/api/endpoints/endpoint.api.ts @@ -0,0 +1,122 @@ +import { autobind } from "../../utils"; +import { KubeObject } from "../kube-object"; +import { KubeApi } from "../kube-api"; + +export interface IEndpointPort { + name?: string; + protocol: string; + port: number; +} + +export interface IEndpointAddress { + hostname: string; + ip: string; + nodeName: string; +} + +export interface IEndpointSubset { + addresses: IEndpointAddress[]; + notReadyAddresses: IEndpointAddress[]; + ports: IEndpointPort[]; +} + +interface ITargetRef { + kind: string; + namespace: string; + name: string; + uid: string; + resourceVersion: string; + apiVersion: string; +} + +export class EndpointAddress implements IEndpointAddress { + hostname: string; + ip: string; + nodeName: string; + targetRef?: { + kind: string; + namespace: string; + name: string; + uid: string; + resourceVersion: string; + }; + + constructor(data: IEndpointAddress) { + Object.assign(this, data) + } + + getId() { + return this.ip + } + + getName() { + return this.hostname + } + + getTargetRef(): ITargetRef { + if (this.targetRef) { + return Object.assign(this.targetRef, {apiVersion: "v1"}) + } else { + return null + } + } +} + +export class EndpointSubset implements IEndpointSubset { + addresses: IEndpointAddress[]; + notReadyAddresses: IEndpointAddress[]; + ports: IEndpointPort[]; + + constructor(data: IEndpointSubset) { + Object.assign(this, data) + } + + getAddresses(): EndpointAddress[] { + const addresses = this.addresses || []; + return addresses.map(a => new EndpointAddress(a)); + } + + getNotReadyAddresses(): EndpointAddress[] { + const notReadyAddresses = this.notReadyAddresses || []; + return notReadyAddresses.map(a => new EndpointAddress(a)); + } + + toString(): string { + if(!this.addresses) { + return "" + } + return this.addresses.map(address => { + return this.ports.map(port => { + return `${address.ip}:${port.port}` + }).join(", ") + }).join(", ") + } +} + +@autobind() +export class Endpoint extends KubeObject { + static kind = "Endpoint" + + subsets: IEndpointSubset[] + + getEndpointSubsets(): EndpointSubset[] { + const subsets = this.subsets || []; + return subsets.map(s => new EndpointSubset(s)); + } + + toString(): string { + if(this.subsets) { + return this.getEndpointSubsets().map(es => es.toString()).join(", ") + } else { + return "" + } + } + +} + +export const endpointApi = new KubeApi({ + kind: Endpoint.kind, + apiBase: "/api/v1/endpoints", + isNamespaced: true, + objectConstructor: Endpoint, +}); diff --git a/dashboard/client/api/endpoints/index.ts b/dashboard/client/api/endpoints/index.ts index e3a893bb47..f175d6feb9 100644 --- a/dashboard/client/api/endpoints/index.ts +++ b/dashboard/client/api/endpoints/index.ts @@ -26,6 +26,7 @@ export * from "./network-policy.api" export * from "./persistent-volume-claims.api" export * from "./persistent-volume.api" export * from "./service.api" +export * from "./endpoint.api" export * from "./storage-class.api" export * from "./pod-metrics.api" export * from "./podsecuritypolicy.api" diff --git a/dashboard/client/components/+network-endpoints/endpoint-details.scss b/dashboard/client/components/+network-endpoints/endpoint-details.scss new file mode 100644 index 0000000000..f0a70bf13a --- /dev/null +++ b/dashboard/client/components/+network-endpoints/endpoint-details.scss @@ -0,0 +1,2 @@ +.EndpointDetails { +} \ No newline at end of file diff --git a/dashboard/client/components/+network-endpoints/endpoint-details.tsx b/dashboard/client/components/+network-endpoints/endpoint-details.tsx new file mode 100644 index 0000000000..158c7674c4 --- /dev/null +++ b/dashboard/client/components/+network-endpoints/endpoint-details.tsx @@ -0,0 +1,44 @@ +import "./endpoint-details.scss" + +import React from "react"; +import { observer } from "mobx-react"; +import { t, Trans } from "@lingui/macro"; +import { DrawerItem, DrawerTitle } from "../drawer"; +import { Badge } from "../badge"; +import { KubeEventDetails } from "../+events/kube-event-details"; +import { KubeObjectDetailsProps } from "../kube-object"; +import { Endpoint, endpointApi } from "../../api/endpoints"; +import { _i18n } from "../../i18n"; +import { apiManager } from "../../api/api-manager"; +import { KubeObjectMeta } from "../kube-object/kube-object-meta"; +import { EndpointSubsetList } from "./endpoint-subset-list"; + +interface Props extends KubeObjectDetailsProps { +} + +@observer +export class EndpointDetails extends React.Component { + render() { + const { object: endpoint } = this.props; + if (!endpoint) return; + return ( +
+ + Subsets}/> + { + endpoint.getEndpointSubsets().map((subset) => { + return( + + ) + }) + } + + +
+ ); + } +} + +apiManager.registerViews(endpointApi, { + Details: EndpointDetails, +}) diff --git a/dashboard/client/components/+network-endpoints/endpoint-subset-list.scss b/dashboard/client/components/+network-endpoints/endpoint-subset-list.scss new file mode 100644 index 0000000000..5742453481 --- /dev/null +++ b/dashboard/client/components/+network-endpoints/endpoint-subset-list.scss @@ -0,0 +1,7 @@ +.EndpointSubsetList { + .title { + margin-top: $margin * 2; + margin-bottom: $margin; + font-weight: bold; + } +} \ No newline at end of file diff --git a/dashboard/client/components/+network-endpoints/endpoint-subset-list.tsx b/dashboard/client/components/+network-endpoints/endpoint-subset-list.tsx new file mode 100644 index 0000000000..4964947151 --- /dev/null +++ b/dashboard/client/components/+network-endpoints/endpoint-subset-list.tsx @@ -0,0 +1,164 @@ +import "./endpoint-subset-list.scss" + +import React from "react"; +import { observer } from "mobx-react"; +import { EndpointSubset, Endpoint, EndpointAddress} from "../../api/endpoints"; +import { _i18n } from "../../i18n"; +import { DrawerItem, DrawerTitle } from "../drawer"; +import { Trans } from "@lingui/macro"; +import { Table, TableCell, TableHead, TableRow } from "../table"; +import { autobind } from "../../utils"; +import { lookupApiLink } from "../../api/kube-api"; +import { getDetailsUrl } from "../../navigation"; +import { Link } from "react-router-dom"; + +interface Props { + subset: EndpointSubset; + endpoint: Endpoint; +} + +@observer +export class EndpointSubsetList extends React.Component { + + getAddressTableRow(ip: string) { + const { subset } = this.props; + const address = subset.getAddresses().find(address => address.getId() == ip); + return this.renderAddressTableRow(address) + } + + @autobind() + getNotReadyAddressTableRow(ip: string) { + const { subset} = this.props; + const address = subset.getNotReadyAddresses().find(address => address.getId() == ip); + return this.renderAddressTableRow(address) + } + + @autobind() + renderAddressTable(addresses: EndpointAddress[], virtual: boolean) { + return ( +
+
Addresses
+ + + IP + Hostname + Target + + { + !virtual && addresses.map(address => this.getAddressTableRow(address.getId())) + } +
+
+ ) + } + + @autobind() + renderAddressTableRow(address: EndpointAddress) { + const { endpoint } = this.props; + return ( + + {address.ip} + {address.hostname} + + { address.targetRef && ( + + {address.targetRef.name} + + )} + + + ); + } + + render() { + const { subset } = this.props; + const addresses = subset.getAddresses(); + const notReadyAddresses = subset.getNotReadyAddresses(); + const addressesVirtual = addresses.length > 100; + const notReadyAddressesVirtual = notReadyAddresses.length > 100; + + return( +
+ {addresses.length > 0 && ( +
+
Addresses
+ + + IP + Hostname + Target + + { !addressesVirtual && addresses.map(address => this.getAddressTableRow(address.getId())) } +
+
+ )} + + {notReadyAddresses.length > 0 && ( +
+
Not Ready Addresses
+ + + IP + Hostname + Target + + { !notReadyAddressesVirtual && notReadyAddresses.map(address => this.getNotReadyAddressTableRow(address.getId())) } +
+
+ )} + +
Ports
+ + + Port + Name + Protocol + + { + subset.ports.map(port => { + return ( + + {port.port} + {port.name} + {port.protocol} + + ); + }) + } +
+
+ ); + } +} diff --git a/dashboard/client/components/+network-endpoints/endpoints.route.ts b/dashboard/client/components/+network-endpoints/endpoints.route.ts new file mode 100644 index 0000000000..932ff5eed5 --- /dev/null +++ b/dashboard/client/components/+network-endpoints/endpoints.route.ts @@ -0,0 +1,11 @@ +import { RouteProps } from "react-router" +import { buildURL } from "../../navigation"; + +export const endpointRoute: RouteProps = { + path: "/endpoints" +} + +export interface EndpointRouteParams { +} + +export const endpointURL = buildURL(endpointRoute.path) diff --git a/dashboard/client/components/+network-endpoints/endpoints.scss b/dashboard/client/components/+network-endpoints/endpoints.scss new file mode 100644 index 0000000000..97d53dbfdd --- /dev/null +++ b/dashboard/client/components/+network-endpoints/endpoints.scss @@ -0,0 +1,7 @@ +.Endpoints { + .TableCell { + &.endpoints { + flex-grow: 5.0; + } + } +} \ No newline at end of file diff --git a/dashboard/client/components/+network-endpoints/endpoints.store.ts b/dashboard/client/components/+network-endpoints/endpoints.store.ts new file mode 100644 index 0000000000..17485ec712 --- /dev/null +++ b/dashboard/client/components/+network-endpoints/endpoints.store.ts @@ -0,0 +1,12 @@ +import { KubeObjectStore } from "../../kube-object.store"; +import { autobind } from "../../utils"; +import { Endpoint, endpointApi } from "../../api/endpoints/endpoint.api"; +import { apiManager } from "../../api/api-manager"; + +@autobind() +export class EndpointStore extends KubeObjectStore { + api = endpointApi +} + +export const endpointStore = new EndpointStore(); +apiManager.registerStore(endpointApi, endpointStore); diff --git a/dashboard/client/components/+network-endpoints/endpoints.tsx b/dashboard/client/components/+network-endpoints/endpoints.tsx new file mode 100644 index 0000000000..cb8e21c8e5 --- /dev/null +++ b/dashboard/client/components/+network-endpoints/endpoints.tsx @@ -0,0 +1,72 @@ +import "./endpoints.scss" + +import * as React from "react" +import { observer } from "mobx-react"; +import { RouteComponentProps } from "react-router-dom" +import { EndpointRouteParams } from "./endpoints.route" +import { Endpoint, endpointApi } from "../../api/endpoints/endpoint.api" +import { endpointStore } from "./endpoints.store"; +import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; +import { KubeObjectListLayout } from "../kube-object"; +import { Trans } from "@lingui/macro"; +import { apiManager } from "../../api/api-manager"; + +enum sortBy { + name = "name", + namespace = "namespace", + age = "age", +} + +interface Props extends RouteComponentProps { +} + +@observer +export class Endpoints extends React.Component { + render() { + return ( + endpoint.getName(), + [sortBy.namespace]: (endpoint: Endpoint) => endpoint.getNs(), + [sortBy.age]: (endpoint: Endpoint) => endpoint.getAge(false), + }} + searchFilters={[ + (endpoint: Endpoint) => endpoint.getSearchFields() + ]} + renderHeaderTitle={Endpoints} + renderTableHeader={[ + { title: Name, className: "name", sortBy: sortBy.name }, + { title: Namespace, className: "namespace", sortBy: sortBy.namespace }, + { title: Endpoints, className: "endpoints" }, + { title: Age, className: "age", sortBy: sortBy.age }, + ]} + renderTableContents={(endpoint: Endpoint) => [ + endpoint.getName(), + endpoint.getNs(), + endpoint.toString(), + endpoint.getAge(), + ]} + renderItemMenu={(item: Endpoint) => { + return + }} + tableProps={{ + customRowHeights: (item: Endpoint, lineHeight, paddings) => { + const lines = item.getEndpointSubsets().length || 1; + return lines * lineHeight + paddings; + } + }} + /> + ) + } +} + +export function EndpointMenu(props: KubeObjectMenuProps) { + return ( + + ) +} + +apiManager.registerViews(endpointApi, { + Menu: EndpointMenu +}) \ No newline at end of file diff --git a/dashboard/client/components/+network-endpoints/index.ts b/dashboard/client/components/+network-endpoints/index.ts new file mode 100644 index 0000000000..9a9da148d6 --- /dev/null +++ b/dashboard/client/components/+network-endpoints/index.ts @@ -0,0 +1,3 @@ +export * from "./endpoints.route" +export * from "./endpoints" +export * from "./endpoint-details" diff --git a/dashboard/client/components/+network-services/service-details-endpoint.tsx b/dashboard/client/components/+network-services/service-details-endpoint.tsx new file mode 100644 index 0000000000..7bcc043431 --- /dev/null +++ b/dashboard/client/components/+network-services/service-details-endpoint.tsx @@ -0,0 +1,49 @@ +import { KubeObject } from "../../api/kube-object"; +import { observer } from "mobx-react"; +import React from "react"; +import { Table, TableHead, TableCell, TableRow } from "../table"; +import { prevDefault } from "../../utils"; +import { showDetails } from "../../navigation"; +import { Trans } from "@lingui/macro"; +import { endpointStore } from "../+network-endpoints/endpoints.store"; +import { Spinner } from "../spinner"; + +interface Props { + endpoint: KubeObject; +} + +@observer +export class ServiceDetailsEndpoint extends React.Component { + render() { + const { endpoint } = this.props + if (!endpoint && !endpointStore.isLoaded) return ( +
+ ); + if (!endpoint) { + return null + } + return ( +
+ + + Name + Endpoints + + showDetails(endpoint.selfLink, false))} + > + {endpoint.getName()} + { endpoint.toString()} + +
+
+ ) + } +} diff --git a/dashboard/client/components/+network-services/service-details.scss b/dashboard/client/components/+network-services/service-details.scss index 661b903955..c5faa712a0 100644 --- a/dashboard/client/components/+network-services/service-details.scss +++ b/dashboard/client/components/+network-services/service-details.scss @@ -1,2 +1,9 @@ .ServicesDetails { + .EndpointList{ + .TableCell { + &.endpoints { + flex-grow: 3; + } + } + } } diff --git a/dashboard/client/components/+network-services/service-details.tsx b/dashboard/client/components/+network-services/service-details.tsx index 84d15f9dfd..0c158c9e6b 100644 --- a/dashboard/client/components/+network-services/service-details.tsx +++ b/dashboard/client/components/+network-services/service-details.tsx @@ -7,21 +7,31 @@ import { DrawerItem, DrawerTitle } from "../drawer"; import { Badge } from "../badge"; import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeObjectDetailsProps } from "../kube-object"; -import { Service, serviceApi } from "../../api/endpoints"; +import { Service, serviceApi, endpointApi } from "../../api/endpoints"; import { _i18n } from "../../i18n"; import { apiManager } from "../../api/api-manager"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { ServicePorts } from "./service-ports"; +import { endpointStore } from "../+network-endpoints/endpoints.store"; +import { ServiceDetailsEndpoint } from "./service-details-endpoint"; interface Props extends KubeObjectDetailsProps { } @observer export class ServiceDetails extends React.Component { + componentDidMount() { + if (!endpointStore.isLoaded) { + endpointStore.loadAll(); + } + endpointApi.watch() + } + render() { const { object: service } = this.props; if (!service) return; const { spec } = service; + const endpoint = endpointStore.getByName(service.getName(), service.getNs()) return (
@@ -59,6 +69,9 @@ export class ServiceDetails extends React.Component { {spec.loadBalancerIP} )} + + +
diff --git a/dashboard/client/components/+network/network.tsx b/dashboard/client/components/+network/network.tsx index 19b64dd488..409bde8fc2 100644 --- a/dashboard/client/components/+network/network.tsx +++ b/dashboard/client/components/+network/network.tsx @@ -7,6 +7,7 @@ import { RouteComponentProps } from "react-router-dom"; import { Trans } from "@lingui/macro"; import { MainLayout, TabRoute } from "../layout/main-layout"; import { Services, servicesRoute, servicesURL } from "../+network-services"; +import { Endpoints, endpointRoute, endpointURL } from "../+network-endpoints"; import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses"; import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies"; import { namespaceStore } from "../+namespaces/namespace.store"; @@ -26,6 +27,12 @@ export class Network extends React.Component { url: servicesURL({ query }), path: servicesRoute.path, }, + { + title: Endpoints, + component: Endpoints, + url: endpointURL({ query }), + path: endpointRoute.path, + }, { title: Ingresses, component: Ingresses,