Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance Ingress details view #3205

Merged
merged 5 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions public/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ common:
resources: Resources
nodeInfo: Node Info
node-details: Node Details
specification: Specification
started-at: Started At
status: Status
value: Value
Expand Down Expand Up @@ -877,17 +878,24 @@ ingresses:
backend: Backend
default-backend: Default Backend
host: Host
host-name: Host name
hosts: Hosts
ingress-class-name: Ingress class name
ip: IP
kind: Kind
load-balancers: Load Balancers
path: Path
path-type: Path type
paths: Paths
port: Port
ports: Ports
port-name: Port name
port-number: Port number
protocol: Protocol
rules: Rules
secret-name: Secret name
service-name: Service name
tls: TLS
message:
rules-not-found: Rules not found
name_singular: Ingress
Expand Down
88 changes: 68 additions & 20 deletions src/resources/Ingresses/IngressDetails.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,54 @@
import { useTranslation } from 'react-i18next';

import { ResourceDetails } from 'shared/components/ResourceDetails/ResourceDetails';
import { EMPTY_TEXT_PLACEHOLDER } from 'shared/constants';

import { Rules } from './Rules';
import { DefaultBackendPanel } from './DefaultBackendPanel';
import IngressCreate from './IngressCreate';
import { ResourceDescription } from 'resources/Ingresses';
import { EventsList } from 'shared/components/EventsList';
import { filterByResource } from 'hooks/useMessageList';
import { StatusBadge } from 'shared/components/StatusBadge/StatusBadge';
import { IngressStatus } from './IngressStatus';
import { IngressSpecification } from './IngressSpecification';
import { useState } from 'react';

export function IngressDetails(props) {
const { t } = useTranslation();
const [totalPods, setTotalPods] = useState(0);
const [healthyPods, setHealthyPods] = useState(0);

const getLoadBalancer = ingress => {
if (ingress.status.loadBalancer?.ingress) {
return ingress.status.loadBalancer?.ingress
.map(endpoint => endpoint.ip || endpoint.hostname)
.join(', ');
} else {
return EMPTY_TEXT_PLACEHOLDER;
}
const calculateTotalPorts = ingress => {
let allPorts = 0;

ingress?.status?.loadBalancer?.ingress?.forEach(element => {
element?.ports?.forEach(() => allPorts++);
});

setTotalPods(allPorts);
};

const customColumns = [
{
header: t('ingresses.labels.load-balancers'),
value: getLoadBalancer,
},
{
header: t('ingresses.labels.ingress-class-name'),
value: ingress => ingress.spec.ingressClassName,
},
];
const calculatePortsWithoutErrors = ingress => {
let healthyPods = 0;

ingress?.status?.loadBalancer?.ingress?.forEach(element => {
element?.ports?.forEach(port => {
if (!port.error) healthyPods++;
});
});

setHealthyPods(healthyPods);
};
Copy link
Contributor

@chriskari chriskari Aug 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can merge these two methods, using a single loop for both

also, is the naming pods vs. ports intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it was a typo :)


const customComponents = [];

customComponents.push(
resource =>
(resource.spec?.ingressClassName || resource.spec?.tls) && (
<IngressSpecification resource={resource} />
),
);

customComponents.push(resource =>
resource.spec.defaultBackend ? (
<DefaultBackendPanel
Expand All @@ -49,12 +64,45 @@ export function IngressDetails(props) {
) : null,
);

customComponents.push(() => (
<EventsList
key="events"
namespace={props.namespace}
filter={filterByResource('Ingress', props.resourceName)}
hideInvolvedObjects={true}
/>
));

const statusBadge = resource => {
calculateTotalPorts(resource);
calculatePortsWithoutErrors(resource);
const portsStatus =
totalPods === 0
? 'Information'
: totalPods === healthyPods
? 'Success'
: 'Error';

return (
<StatusBadge
type={portsStatus}
tooltipContent={'Healthy Ports'}
>{`${healthyPods} / ${totalPods}`}</StatusBadge>
);
};

return (
<ResourceDetails
customColumns={customColumns}
customComponents={customComponents}
description={ResourceDescription}
createResourceForm={IngressCreate}
statusBadge={statusBadge}
customConditionsComponents={[
{
header: t('ingresses.labels.load-balancers'),
value: resource => <IngressStatus resource={resource} />,
},
]}
{...props}
/>
);
Expand Down
39 changes: 39 additions & 0 deletions src/resources/Ingresses/IngressSpecification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useTranslation } from 'react-i18next';
import { GenericList } from 'shared/components/GenericList/GenericList';
import { LayoutPanelRow } from 'shared/components/LayoutPanelRow/LayoutPanelRow';
import { UI5Panel } from 'shared/components/UI5Panel/UI5Panel';
import { EMPTY_TEXT_PLACEHOLDER } from 'shared/constants';

export const IngressSpecification = ({ resource }) => {
const { t } = useTranslation();

return (
<>
<UI5Panel title={t('common.headers.specification')}>
{resource.spec.ingressClassName && (
<LayoutPanelRow
name={t('ingresses.labels.ingress-class-name')}
value={resource.spec.ingressClassName}
/>
)}
{resource.spec?.tls && (
<GenericList
title={t('ingresses.labels.tls')}
headerRenderer={() => [
t('ingresses.labels.hosts'),
t('ingresses.labels.secret-name'),
]}
rowRenderer={tls => [
tls?.hosts?.join(', ') ?? EMPTY_TEXT_PLACEHOLDER,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to use the <Tokens> component here instead of joining strings

tls?.secretName ?? EMPTY_TEXT_PLACEHOLDER,
]}
entries={resource.spec?.tls}
searchSettings={{
showSearchField: false,
}}
/>
)}
</UI5Panel>
</>
);
};
107 changes: 107 additions & 0 deletions src/resources/Ingresses/IngressStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ConditionList } from 'shared/components/ConditionList/ConditionList';
import { EMPTY_TEXT_PLACEHOLDER } from 'shared/constants';
import { spacing } from '@ui5/webcomponents-react-base';
import './IngressStatus.scss';

export const IngressStatus = ({ resource }) => {
const { t } = useTranslation();
const ingresses = useMemo(() => {
return resource?.status?.loadBalancer?.ingress?.map(ingress => ingress);
}, [resource]);

return ingresses ? (
<ConditionList
className="load-balancers"
conditions={ingresses.map(ingress => ({
header: {
titleText: ingress.hostname ? (
<div>
<span
style={{ ...spacing.sapUiTinyMarginEnd }}
className="title bsl-has-color-status-4"
>
{`${t('ingresses.labels.host-name')}:`}
</span>
{`${ingress.hostname}`}
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we replace this somehow with a UI5 component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, that's how we are doing it in metadata and status - Labels have slightly different color

) : (
<div>
<span
style={{ ...spacing.sapUiTinyMarginEnd }}
className="title bsl-has-color-status-4"
>
{`${t('ingresses.labels.ip')}:`}
</span>
{`${ingress.ip}`}
</div>
),
},
customContent: [
...(ingress.hostname
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as I see we can remove those spread operators here and below and simply not place the resulting object inside of an array

? [
{
header: t('ingresses.labels.host-name'),
value: ingress.hostname,
className: 'load-balancers__content',
},
]
: []),
...(ingress.ip
? [
{
header: t('ingresses.labels.ip'),
value: ingress.ip,
className: 'load-balancers__content',
},
]
: []),
{
header: t('ingresses.labels.ports'),
value: ingress.ports ? (
<ConditionList
conditions={ingress?.ports?.map(port => {
return {
header: {
titleText: port.port,
status: port.error ? 'Error' : '',
},
customContent: [
port.protocol
? {
header: t('ingresses.labels.protocol'),
value: port.protocol,
}
: {},
...(port.error
? [
{
header: 'Error',
value: port.error,
},
]
: []),
],
};
})}
/>
) : (
EMPTY_TEXT_PLACEHOLDER
),
},
],
}))}
/>
) : (
<div
className="content bsl-has-color-text-1"
style={{
...spacing.sapUiSmallMarginBegin,
...spacing.sapUiSmallMarginBottom,
}}
>
{EMPTY_TEXT_PLACEHOLDER}
</div>
);
};
11 changes: 11 additions & 0 deletions src/resources/Ingresses/IngressStatus.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.load-balancers {
.expandable-item__message:not(.load-balancers__content) {
.title {
margin-left: 0.5rem !important;
}
}

&__content {
margin-left: 1.5rem !important;
}
}
13 changes: 9 additions & 4 deletions src/shared/components/ConditionList/ConditionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,32 @@ import {
CustomContent,
ExpandableListItem,
} from '../ExpandableListItem/ExpandableListItem';
import { ReactNode } from 'react';

type ConditionListProps = {
conditions: [ConditionItem];
className?: string;
};

type ConditionItem = {
message: string;
header: ConditionHeader;
message?: string;
customContent?: CustomContent[];
};
type ConditionHeader = {
titleText: string;
titleText: string | ReactNode;
status?: string;
};

export const ConditionList = ({ conditions }: ConditionListProps) => {
export const ConditionList = ({
conditions,
className,
}: ConditionListProps) => {
if (!conditions) {
return null;
}
return (
<List>
<List className={className}>
{conditions?.map((cond, index) => (
<ExpandableListItem
key={index}
Expand Down
Loading
Loading