diff --git a/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraph.tsx b/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraph.tsx
index 2fb3406b217b2..cb3694f896426 100644
--- a/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraph.tsx
+++ b/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraph.tsx
@@ -26,6 +26,7 @@ import CidrBlockSideBar from './cidr/CidrBlockSideBar';
import ExternalEntitiesSideBar from './external/ExternalEntitiesSideBar';
import './Topology.css';
+import { getNodeById } from './utils/networkGraphUtils';
// TODO: move these type defs to a central location
export const UrlDetailType = {
@@ -38,11 +39,6 @@ export const UrlDetailType = {
export type UrlDetailTypeKey = keyof typeof UrlDetailType;
export type UrlDetailTypeValue = typeof UrlDetailType[UrlDetailTypeKey];
-function findEntityById(graphModel: Model, id: string | undefined): NodeModel | undefined {
- const entity = graphModel.nodes?.find((node: { id: string }) => node.id === id);
- return entity;
-}
-
function getUrlParamsForEntity(selectedEntity: NodeModel): [UrlDetailTypeValue, string] {
const detailType = UrlDetailType[selectedEntity.data.type];
const detailId = selectedEntity.id;
@@ -95,7 +91,7 @@ function setEdges(controller, detailId) {
const TopologyComponent = ({ model }: TopologyComponentProps) => {
const history = useHistory();
const { detailId } = useParams();
- const selectedEntity = detailId && findEntityById(model, detailId);
+ const selectedEntity = detailId && getNodeById(model, detailId);
const controller = useVisualizationController();
// to prevent error where graph hasn't initialized yet
@@ -109,7 +105,7 @@ const TopologyComponent = ({ model }: TopologyComponentProps) => {
function onSelect(ids: string[]) {
const newSelectedId = ids?.[0] || '';
- const newSelectedEntity = findEntityById(model, newSelectedId);
+ const newSelectedEntity = getNodeById(model, newSelectedId);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (newSelectedEntity) {
@@ -132,6 +128,7 @@ const TopologyComponent = ({ model }: TopologyComponentProps) => {
return () => {
controller.removeEventListener(SELECTION_EVENT, onSelect);
};
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [controller, model]);
const selectedIds = selectedEntity ? [selectedEntity.id] : [];
@@ -144,7 +141,11 @@ const TopologyComponent = ({ model }: TopologyComponentProps) => {
)}
{selectedEntity && selectedEntity?.data?.type === 'DEPLOYMENT' && (
-
+
)}
{selectedEntity && selectedEntity?.data?.type === 'CIDR_BLOCK' && (
diff --git a/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraphPage.tsx b/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraphPage.tsx
index 6a81bdb3e490d..f04219e02b62e 100644
--- a/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraphPage.tsx
+++ b/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraphPage.tsx
@@ -7,7 +7,7 @@ import { fetchClustersAsArray, Cluster } from 'services/ClustersService';
import PageTitle from 'Components/PageTitle';
import NetworkGraph from './NetworkGraph';
-import { transformData, graphModel } from './utils';
+import { transformData, graphModel } from './utils/modelUtils';
import './NetworkGraphPage.css';
diff --git a/ui/apps/platform/src/Containers/NetworkGraph/deployment/DeploymentDetails.tsx b/ui/apps/platform/src/Containers/NetworkGraph/deployment/DeploymentDetails.tsx
index 0f384f6fb81d0..acec599fdf362 100644
--- a/ui/apps/platform/src/Containers/NetworkGraph/deployment/DeploymentDetails.tsx
+++ b/ui/apps/platform/src/Containers/NetworkGraph/deployment/DeploymentDetails.tsx
@@ -18,6 +18,18 @@ import {
TextVariants,
} from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
+import pluralize from 'pluralize';
+
+import { Deployment } from 'types/deployment.proto';
+import { ListenPort } from 'types/networkFlow.proto';
+import { getDateTime } from 'utils/dateUtils';
+
+type DeploymentDetailsProps = {
+ deployment: Deployment;
+ numExternalFlows: number;
+ numInternalFlows: number;
+ listenPorts: ListenPort[];
+};
function DetailSection({ title, children }) {
const [isExpanded, setIsExpanded] = useState(true);
@@ -43,7 +55,12 @@ function DetailSection({ title, children }) {
);
}
-function DeploymentDetails() {
+function DeploymentDetails({
+ deployment,
+ numExternalFlows,
+ numInternalFlows,
+ listenPorts,
+}: DeploymentDetailsProps) {
return (
@@ -63,7 +80,8 @@ function DeploymentDetails() {
color="red"
icon={}
>
- 2 external flows
+ {numExternalFlows} external{' '}
+ {pluralize('flow', numExternalFlows)}
@@ -72,7 +90,8 @@ function DeploymentDetails() {
color="gold"
icon={}
>
- 3 internal flows
+ {numInternalFlows} internal{' '}
+ {pluralize('flow', numInternalFlows)}
@@ -110,7 +129,19 @@ function DeploymentDetails() {
alignItems={{ default: 'alignItemsCenter' }}
>
-
+
+ {listenPorts.map(({ port, l4protocol }) => {
+ const protocol = l4protocol.replace(
+ 'L4_PROTOCOL_',
+ ''
+ );
+ return (
+
+ );
+ })}
+
@@ -128,21 +159,21 @@ function DeploymentDetails() {
Name
Created
- 12/09/21 | 6:03:23 PM
+ {getDateTime(deployment.created)}
Cluster
@@ -150,7 +181,7 @@ function DeploymentDetails() {
Namespace
@@ -158,7 +189,7 @@ function DeploymentDetails() {
Replicas
@@ -166,7 +197,7 @@ function DeploymentDetails() {
Service account
@@ -178,10 +209,15 @@ function DeploymentDetails() {
Labels
-
-
+ {Object.keys(deployment.labels).map((labelKey) => {
+ const labelValue = deployment.labels[labelKey];
+ const label = `${labelKey}:${labelValue}`;
+ return (
+
+ );
+ })}
@@ -189,33 +225,23 @@ function DeploymentDetails() {
Annotations
-
-
+ {Object.keys(deployment.annotations).map(
+ (annotationKey) => {
+ const annotationValue =
+ deployment.annotations[annotationKey];
+ const annotation = `${annotationKey}:${annotationValue}`;
+ return (
+
+ );
+ }
+ )}
-
-
-
- AddCapabilities
-
- SYS_ADMIN
-
-
-
- Privileged
-
- true
-
-
-
-
diff --git a/ui/apps/platform/src/Containers/NetworkGraph/deployment/DeploymentSideBar.tsx b/ui/apps/platform/src/Containers/NetworkGraph/deployment/DeploymentSideBar.tsx
index e84b365359693..0aeee27bf3f22 100644
--- a/ui/apps/platform/src/Containers/NetworkGraph/deployment/DeploymentSideBar.tsx
+++ b/ui/apps/platform/src/Containers/NetworkGraph/deployment/DeploymentSideBar.tsx
@@ -1,8 +1,12 @@
import React from 'react';
import {
+ Alert,
+ AlertVariant,
Badge,
+ Bullseye,
Flex,
FlexItem,
+ Spinner,
Stack,
StackItem,
Tab,
@@ -13,17 +17,52 @@ import {
TextContent,
TextVariants,
} from '@patternfly/react-core';
+import { EdgeModel, NodeModel } from '@patternfly/react-topology';
import useTabs from 'hooks/patternfly/useTabs';
+import useFetchDeployment from 'hooks/useFetchDeployment';
+import {
+ getListenPorts,
+ getNumExternalFlows,
+ getNumInternalFlows,
+} from '../utils/networkGraphUtils';
+
import DeploymentDetails from './DeploymentDetails';
import DeploymentNetworkPolicies from './DeploymentNetworkPolicies';
import DeploymentFlows from './DeploymentFlows';
-function DeploymentSideBar() {
+type DeploymentSideBarProps = {
+ deploymentId: string;
+ nodes: NodeModel[];
+ edges: EdgeModel[];
+};
+
+function DeploymentSideBar({ deploymentId, nodes, edges }: DeploymentSideBarProps) {
+ // component state
+ const { deployment, isLoading, error } = useFetchDeployment(deploymentId);
const { activeKeyTab, onSelectTab } = useTabs({
defaultTab: 'Details',
});
+ // derived values
+ const numExternalFlows = getNumExternalFlows(nodes, edges, deploymentId);
+ const numInternalFlows = getNumInternalFlows(nodes, edges, deploymentId);
+ const listenPorts = getListenPorts(nodes, deploymentId);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
return (
@@ -34,7 +73,7 @@ function DeploymentSideBar() {
- visa-processor
+ {deployment?.name}
@@ -42,7 +81,7 @@ function DeploymentSideBar() {
component={TextVariants.h2}
className="pf-u-font-size-sm pf-u-color-200"
>
- in "production / naples"
+ in "{deployment?.clusterName} / {deployment?.namespace}"
@@ -74,7 +113,14 @@ function DeploymentSideBar() {
-
+ {deployment && (
+
+ )}
diff --git a/ui/apps/platform/src/Containers/NetworkGraph/utils.tsx b/ui/apps/platform/src/Containers/NetworkGraph/utils/modelUtils.tsx
similarity index 84%
rename from ui/apps/platform/src/Containers/NetworkGraph/utils.tsx
rename to ui/apps/platform/src/Containers/NetworkGraph/utils/modelUtils.tsx
index fa26d34cf7483..092bee990da6f 100644
--- a/ui/apps/platform/src/Containers/NetworkGraph/utils.tsx
+++ b/ui/apps/platform/src/Containers/NetworkGraph/utils/modelUtils.tsx
@@ -2,7 +2,7 @@ import { Model, NodeModel, EdgeModel } from '@patternfly/react-topology';
import { NetworkEntityInfo, Node } from 'types/networkFlow.proto';
-function getLabel(entity: NetworkEntityInfo): string {
+function getNameByEntity(entity: NetworkEntityInfo): string {
const { type } = entity;
switch (type) {
case 'DEPLOYMENT':
@@ -29,15 +29,19 @@ export function transformData(nodes: Node[]): Model {
edges: [] as EdgeModel[],
};
const groupNodes = {} as NodeModel;
- nodes.forEach(({ entity, outEdges }) => {
+ nodes.forEach(({ entity, policyIds, outEdges }) => {
// creating each node and adding to data model
const node = {
id: entity.id,
type: 'node',
width: 75,
height: 75,
- label: getLabel(entity),
- data: entity,
+ label: getNameByEntity(entity),
+ // @TODO: create a consistent data structure for "data" between all nodes
+ data: {
+ ...entity,
+ policyIds,
+ },
};
dataModel.nodes.push(node);
@@ -54,6 +58,7 @@ export function transformData(nodes: Node[]): Model {
group: true,
label: namespace,
style: { padding: 15 },
+ // @TODO: create a consistent data structure for "data" between all nodes
data: {
collapsible: true,
showContextMenu: false,
diff --git a/ui/apps/platform/src/Containers/NetworkGraph/utils/networkGraphUtils.ts b/ui/apps/platform/src/Containers/NetworkGraph/utils/networkGraphUtils.ts
new file mode 100644
index 0000000000000..a0b37ee49803f
--- /dev/null
+++ b/ui/apps/platform/src/Containers/NetworkGraph/utils/networkGraphUtils.ts
@@ -0,0 +1,72 @@
+import { EdgeModel, Model, NodeModel } from '@patternfly/react-topology';
+
+import { ListenPort } from 'types/networkFlow.proto';
+
+/* node helper functions */
+
+function getExternalNodeIds(nodes: NodeModel[]): string[] {
+ const externalNodeIds =
+ nodes?.reduce((acc, curr) => {
+ if (curr.data.type === 'INTERNET' || curr.data.type === 'EXTERNAL_SOURCE') {
+ return [...acc, curr.id];
+ }
+ return acc;
+ }, [] as string[]) || [];
+ return externalNodeIds;
+}
+
+export function getNodeById(model: Model, nodeId: string | undefined): NodeModel | undefined {
+ return model.nodes?.find((node) => node.id === nodeId);
+}
+
+/* edge helper functions */
+
+export function getNumInternalFlows(
+ nodes: NodeModel[],
+ edges: EdgeModel[],
+ deploymentId: string
+): number {
+ const externalNodeIds = getExternalNodeIds(nodes);
+ const numInternalFlows =
+ edges?.reduce((acc, edge) => {
+ if (
+ (edge.source === deploymentId && !externalNodeIds.includes(edge.target || '')) ||
+ (edge.target === deploymentId && !externalNodeIds.includes(edge.source || ''))
+ ) {
+ return acc + 1;
+ }
+ return acc;
+ }, 0) || 0;
+ return numInternalFlows;
+}
+
+export function getNumExternalFlows(
+ nodes: NodeModel[],
+ edges: EdgeModel[],
+ deploymentId: string
+): number {
+ const externalNodeIds = getExternalNodeIds(nodes);
+ const numExternalFlows =
+ edges?.reduce((acc, edge) => {
+ if (
+ (edge.source === deploymentId && externalNodeIds.includes(edge.target || '')) ||
+ (edge.target === deploymentId && externalNodeIds.includes(edge.source || ''))
+ ) {
+ return acc + 1;
+ }
+ return acc;
+ }, 0) || 0;
+ return numExternalFlows;
+}
+
+/* deployment helper functions */
+
+export function getListenPorts(nodes: NodeModel[], deploymentId: string): ListenPort[] {
+ const deployment = nodes?.find((node) => {
+ return node.id === deploymentId;
+ });
+ if (!deployment) {
+ return [];
+ }
+ return deployment.data.deployment.listenPorts as ListenPort[];
+}
diff --git a/ui/apps/platform/src/hooks/useFetchDeployment.ts b/ui/apps/platform/src/hooks/useFetchDeployment.ts
new file mode 100644
index 0000000000000..b5bcd4d226281
--- /dev/null
+++ b/ui/apps/platform/src/hooks/useFetchDeployment.ts
@@ -0,0 +1,33 @@
+import { useEffect, useState } from 'react';
+import { fetchDeployment } from 'services/DeploymentsService';
+
+import { Deployment } from 'types/deployment.proto';
+
+type Result = { isLoading: boolean; deployment: Deployment | null; error: string | null };
+
+const defaultResultState = { deployment: null, error: null, isLoading: true };
+
+/*
+ * This hook does an API call to the deployment API to get a deployment
+ */
+function useFetchDeployment(deploymentId: string): Result {
+ const [result, setResult] = useState(defaultResultState);
+
+ useEffect(() => {
+ setResult(defaultResultState);
+
+ if (deploymentId) {
+ fetchDeployment(deploymentId)
+ .then((data) => {
+ setResult({ deployment: data || null, error: null, isLoading: false });
+ })
+ .catch((error) => {
+ setResult({ deployment: null, error, isLoading: false });
+ });
+ }
+ }, [deploymentId]);
+
+ return result;
+}
+
+export default useFetchDeployment;