diff --git a/frontend/src/components/Sidebar/__snapshots__/Sidebar.InClusterSidebarClosed.stories.storyshot b/frontend/src/components/Sidebar/__snapshots__/Sidebar.InClusterSidebarClosed.stories.storyshot index 0b4b6ee9318..4887c8279b3 100644 --- a/frontend/src/components/Sidebar/__snapshots__/Sidebar.InClusterSidebarClosed.stories.storyshot +++ b/frontend/src/components/Sidebar/__snapshots__/Sidebar.InClusterSidebarClosed.stories.storyshot @@ -592,6 +592,138 @@ +
  • + +
    + + +
  • +
  • +
    + +
    +
  • diff --git a/frontend/src/components/Sidebar/__snapshots__/Sidebar.InClusterSidebarOpen.stories.storyshot b/frontend/src/components/Sidebar/__snapshots__/Sidebar.InClusterSidebarOpen.stories.storyshot index 1060221ac96..d9b1461831f 100644 --- a/frontend/src/components/Sidebar/__snapshots__/Sidebar.InClusterSidebarOpen.stories.storyshot +++ b/frontend/src/components/Sidebar/__snapshots__/Sidebar.InClusterSidebarOpen.stories.storyshot @@ -627,6 +627,145 @@
  • +
  • + +
    +
    + + Gateway + +
    + +
    +
  • +
  • +
    + +
    +
  • diff --git a/frontend/src/components/Sidebar/__snapshots__/Sidebar.SelectedItemWithSidebarOmitted.stories.storyshot b/frontend/src/components/Sidebar/__snapshots__/Sidebar.SelectedItemWithSidebarOmitted.stories.storyshot index 24473bbe9cb..8c4d7b09d1b 100644 --- a/frontend/src/components/Sidebar/__snapshots__/Sidebar.SelectedItemWithSidebarOmitted.stories.storyshot +++ b/frontend/src/components/Sidebar/__snapshots__/Sidebar.SelectedItemWithSidebarOmitted.stories.storyshot @@ -627,6 +627,145 @@
  • +
  • + +
    +
    + + Gateway + +
    + +
    +
  • +
  • +
    + +
    +
  • diff --git a/frontend/src/components/Sidebar/prepareRoutes.ts b/frontend/src/components/Sidebar/prepareRoutes.ts index 9216abeee84..7b08045723c 100644 --- a/frontend/src/components/Sidebar/prepareRoutes.ts +++ b/frontend/src/components/Sidebar/prepareRoutes.ts @@ -166,6 +166,29 @@ function prepareRoutes( }, ], }, + { + name: 'gateway', + label: t('glossary|Gateway'), + icon: 'mdi:lan-connect', + subList: [ + { + name: 'k8sgateways', + label: t('glossary|Gateways'), + }, + { + name: 'gatewayclasses', + label: t('glossary|Gateway Classes'), + }, + { + name: 'httproutes', + label: t('glossary|HTTP Routes'), + }, + { + name: 'grpcroutes', + label: t('glossary|GRPC Routes'), + }, + ], + }, { name: 'security', label: t('glossary|Security'), diff --git a/frontend/src/components/gateway/ClassDetails.stories.tsx b/frontend/src/components/gateway/ClassDetails.stories.tsx new file mode 100644 index 00000000000..aca1d77fb4a --- /dev/null +++ b/frontend/src/components/gateway/ClassDetails.stories.tsx @@ -0,0 +1,60 @@ +import { Meta, StoryFn } from '@storybook/react'; +import { http, HttpResponse } from 'msw'; +import { TestContext } from '../../test'; +import Details from './ClassDetails'; +import { RESOURCE_GATEWAY_CLASS } from './storyHelper'; + +export default { + title: 'GatewayClass/DetailsView', + component: Details, + argTypes: {}, + decorators: [ + Story => { + return ( + + + + ); + }, + ], + parameters: { + msw: { + handlers: { + storyBase: [ + http.get( + 'http://localhost:4466/apis/gateway.networking.k8s.io/v1beta1/gatewayclasses', + () => HttpResponse.error() + ), + http.get('http://localhost:4466/api/v1/namespaces/default/events', () => + HttpResponse.json({ + kind: 'EventList', + items: [], + metadata: {}, + }) + ), + ], + }, + }, + }, +} as Meta; + +const Template: StoryFn = () => { + return
    ; +}; + +export const Basic = Template.bind({}); +Basic.args = { + gatewayJson: RESOURCE_GATEWAY_CLASS, +}; +Basic.parameters = { + msw: { + handlers: { + story: [ + http.get( + 'http://localhost:4466/apis/gateway.networking.k8s.io/v1beta1/gatewayclasses/resource-example-ingress', + () => HttpResponse.json(RESOURCE_GATEWAY_CLASS) + ), + ], + }, + }, +}; diff --git a/frontend/src/components/gateway/ClassDetails.tsx b/frontend/src/components/gateway/ClassDetails.tsx new file mode 100644 index 00000000000..a35e1e88035 --- /dev/null +++ b/frontend/src/components/gateway/ClassDetails.tsx @@ -0,0 +1,38 @@ +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import GatewayClass, { KubeGatewayClass } from '../../lib/k8s/gatewayClass'; +import { ConditionsTable, DetailsGrid } from '../common/Resource'; +import SectionBox from '../common/SectionBox'; + +export default function GatewayClassDetails() { + const { name } = useParams<{ name: string }>(); + const { t } = useTranslation(['glossary', 'translation']); + + return ( + + gatewayClass && [ + { + name: t('Controller Name'), + value: gatewayClass.controllerName, + }, + ] + } + extraSections={(item: KubeGatewayClass) => + item && [ + { + id: 'headlamp.gatewayclass-conditions', + section: ( + + + + ), + }, + ] + } + /> + ); +} diff --git a/frontend/src/components/gateway/ClassList.stories.tsx b/frontend/src/components/gateway/ClassList.stories.tsx new file mode 100644 index 00000000000..e9333ce66a4 --- /dev/null +++ b/frontend/src/components/gateway/ClassList.stories.tsx @@ -0,0 +1,43 @@ +import { Meta, StoryFn } from '@storybook/react'; +import { http, HttpResponse } from 'msw'; +import { TestContext } from '../../test'; +import ListView from './ClassList'; +import { RESOURCE_GATEWAY_CLASS } from './storyHelper'; + +export default { + title: 'GatewayClass/ListView', + component: ListView, + argTypes: {}, + decorators: [ + Story => { + return ( + + + + ); + }, + ], + parameters: { + msw: { + handlers: { + story: [ + http.get( + 'http://localhost:4466/apis/gateway.networking.k8s.io/v1beta1/gatewayclasses', + () => + HttpResponse.json({ + kind: 'GatewayClassList', + metadata: {}, + items: [RESOURCE_GATEWAY_CLASS], + }) + ), + ], + }, + }, + }, +} as Meta; + +const Template: StoryFn = () => { + return ; +}; + +export const Items = Template.bind({}); diff --git a/frontend/src/components/gateway/ClassList.tsx b/frontend/src/components/gateway/ClassList.tsx new file mode 100644 index 00000000000..8d0696d1570 --- /dev/null +++ b/frontend/src/components/gateway/ClassList.tsx @@ -0,0 +1,75 @@ +import { Box } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import GatewayClass from '../../lib/k8s/gatewayClass'; +import { LightTooltip, StatusLabel, StatusLabelProps } from '../common'; +import ResourceListView from '../common/Resource/ResourceListView'; + +export function makeGatewayStatusLabel(conditions: any[] | null) { + if (!conditions) { + return null; + } + + const conditionOptions = { + Accepted: { + status: 'success', + icon: 'mdi:check-bold', + }, + }; + + const condition = conditions.find( + ({ status, type }: { status: string; type: string }) => + type in conditionOptions && status === 'True' + ); + + if (!condition) { + return null; + } + + const tooltip = ''; + + const conditionInfo = conditionOptions[condition.type as 'Accepted']; + + return ( + + + + {condition.type} + + + + ); +} + +export default function GatewayClassList() { + const { t } = useTranslation('glossary'); + + return ( + gatewayClass.spec?.controllerName, + }, + { + id: 'conditions', + label: t('translation|Conditions'), + getValue: (gatewayClass: GatewayClass) => + gatewayClass.status?.conditions?.find( + ({ status }: { status: string }) => status === 'True' + ) ?? null, + render: (gatewayClass: GatewayClass) => + makeGatewayStatusLabel(gatewayClass.status?.conditions), + }, + 'age', + ]} + /> + ); +} diff --git a/frontend/src/components/gateway/GRPCRouteDetails.tsx b/frontend/src/components/gateway/GRPCRouteDetails.tsx new file mode 100644 index 00000000000..0457e049df6 --- /dev/null +++ b/frontend/src/components/gateway/GRPCRouteDetails.tsx @@ -0,0 +1,67 @@ +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import GRPCRoute from '../../lib/k8s/grpcRoute'; +import { GatewayParentReference } from '../../lib/k8s/httpRoute'; +import { Link, SimpleTable } from '../common'; +import { DetailsGrid } from '../common/Resource'; +import SectionBox from '../common/SectionBox'; + +export default function GRPCRouteDetails(props: { name?: string; namespace?: string }) { + const params = useParams<{ namespace: string; name: string }>(); + const { name = params.name, namespace = params.namespace } = props; + const { t } = useTranslation(['glossary', 'translation']); + + return ( + + item && [ + { + id: 'headlamp.grpcroute-parentrefs', + section: ( + + ( + + {data.name} + + ), + }, + { + label: t('translation|Namespace'), + getter: (data: GatewayParentReference) => data.namespace, + }, + { + label: t('translation|Kind'), + getter: (data: GatewayParentReference) => data.kind, + }, + { + label: t('translation|Group'), + getter: (data: GatewayParentReference) => data.group, + }, + ]} + data={item?.parentRefs || []} + reflectInURL="listeners" + /> + + ), + }, + ] + } + /> + ); +} diff --git a/frontend/src/components/gateway/GRPCRouteList.tsx b/frontend/src/components/gateway/GRPCRouteList.tsx new file mode 100644 index 00000000000..6472010741a --- /dev/null +++ b/frontend/src/components/gateway/GRPCRouteList.tsx @@ -0,0 +1,25 @@ +import { useTranslation } from 'react-i18next'; +import GRPCRoute from '../../lib/k8s/grpcRoute'; +import ResourceListView from '../common/Resource/ResourceListView'; + +export default function GRPCRouteList() { + const { t } = useTranslation(['glossary', 'translation']); + + return ( + httpRoute.spec.rules.length, + }, + 'age', + ]} + /> + ); +} diff --git a/frontend/src/components/gateway/GatewayDetails.stories.tsx b/frontend/src/components/gateway/GatewayDetails.stories.tsx new file mode 100644 index 00000000000..e6c9b418dbe --- /dev/null +++ b/frontend/src/components/gateway/GatewayDetails.stories.tsx @@ -0,0 +1,66 @@ +import { Meta, StoryFn } from '@storybook/react'; +import { http, HttpResponse } from 'msw'; +import { TestContext } from '../../test'; +import GatewayDetails from './GatewayDetails'; +import { DEFAULT_GATEWAY } from './storyHelper'; + +export default { + title: 'Gateway/DetailsView', + component: GatewayDetails, + argTypes: {}, + decorators: [ + Story => { + return ( + + + + ); + }, + ], + parameters: { + msw: { + handlers: { + baseStory: [ + http.get('http://localhost:4466/apis/gateway.networking.k8s.io/v1beta1/gateways', () => + HttpResponse.json({}) + ), + http.get('http://localhost:4466/apis/gateway.networking.k8s.io/v1beta1/gateways', () => + HttpResponse.error() + ), + http.get('http://localhost:4466/api/v1/namespaces/default/events', () => + HttpResponse.json({ + kind: 'EventList', + items: [], + metadata: {}, + }) + ), + http.post( + 'http://localhost:4466/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', + () => HttpResponse.json({ status: { allowed: true, reason: '', code: 200 } }) + ), + ], + }, + }, + }, +} as Meta; + +const Template: StoryFn = () => { + return ; +}; + +export const Basic = Template.bind({}); +Basic.args = { + gatewayJson: DEFAULT_GATEWAY, +}; +Basic.parameters = { + msw: { + handlers: { + story: [ + http.get( + 'http://localhost:4466/apis/gateway.networking.k8s.io/v1beta1/gateways/my-ingress', + () => HttpResponse.json(DEFAULT_GATEWAY) + ), + ], + }, + }, +}; diff --git a/frontend/src/components/gateway/GatewayDetails.tsx b/frontend/src/components/gateway/GatewayDetails.tsx new file mode 100644 index 00000000000..e6574e4d469 --- /dev/null +++ b/frontend/src/components/gateway/GatewayDetails.tsx @@ -0,0 +1,134 @@ +import Box from '@mui/system/Box'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { KubeCondition } from '../../lib/k8s/cluster'; +import Gateway, { + GatewayAddress, + GatewayListener, + GatewayListenerStatus, +} from '../../lib/k8s/gateway'; +import { EmptyContent, StatusLabel, StatusLabelProps } from '../common'; +import Link from '../common/Link'; +import { ConditionsTable, DetailsGrid } from '../common/Resource'; +import SectionBox from '../common/SectionBox'; +import SimpleTable, { NameValueTable } from '../common/SimpleTable'; + +function GatewayListenerTable(props: { + listener: GatewayListener; + status: GatewayListenerStatus | null; +}) { + const { listener, status } = props; + const { t } = useTranslation(['glossary', 'translation']); + + function makeStatusLabel(condition: KubeCondition) { + let status: StatusLabelProps['status'] = ''; + if (condition.type === 'Available') { + status = condition.status === 'True' ? 'success' : 'error'; + } + + return ( + ({ paddingRight: theme.spacing(1) })}> + {condition.type} + + ); + } + const mainRows = [ + { + name: listener.name, + withHighlightStyle: true, + }, + { + name: t('translation|Hostname'), + value: listener.hostname, + }, + { + name: t('translation|Protocol'), + value: listener.protocol, + }, + { + name: t('translation|Conditions'), + value: status?.conditions.map(c => makeStatusLabel(c)), + }, + ]; + return ; +} + +export default function GatewayDetails(props: { name?: string; namespace?: string }) { + const params = useParams<{ namespace: string; name: string }>(); + const { name = params.name, namespace = params.namespace } = props; + const { t } = useTranslation(['glossary', 'translation']); + + return ( + + gateway && [ + { + name: t('Class Name'), + value: gateway.spec?.gatewayClassName ? ( + + {gateway.spec?.gatewayClassName} + + ) : null, + }, + ] + } + extraSections={(item: Gateway) => + item && [ + { + id: 'headlamp.gateway-addresses', + section: item && ( + + data.type, + }, + { + label: t('translation|Value'), + getter: (data: GatewayAddress) => data.value, + }, + ]} + data={item?.getAddresses() || []} + reflectInURL="addresses" + /> + + ), + }, + { + id: 'headlamp.gateway-listeners', + section: item && ( + + {item.getListeners().length === 0 ? ( + {t('No data in this config map')} + ) : ( + item + .getListeners() + .map((listener: GatewayListener) => ( + + )) + )} + + ), + }, + { + id: 'headlamp.gateway-conditions', + section: ( + + + + ), + }, + ] + } + /> + ); +} diff --git a/frontend/src/components/gateway/GatewayList.stories.tsx b/frontend/src/components/gateway/GatewayList.stories.tsx new file mode 100644 index 00000000000..aeecc6862c0 --- /dev/null +++ b/frontend/src/components/gateway/GatewayList.stories.tsx @@ -0,0 +1,45 @@ +import { Meta, StoryFn } from '@storybook/react'; +import { http, HttpResponse } from 'msw'; +import { TestContext } from '../../test'; +import ListView from './GatewayList'; +import { DEFAULT_GATEWAY } from './storyHelper'; + +export default { + title: 'Gateway/ListView', + component: ListView, + argTypes: {}, + decorators: [ + Story => { + return ( + + + + ); + }, + ], + parameters: { + msw: { + handlers: { + storyBase: [], + story: [ + http.get('http://localhost:4466/apis/gateway.networking.k8s.io/v1beta1/gateways', () => + HttpResponse.json({ + kind: 'GatewayList', + metadata: {}, + items: [DEFAULT_GATEWAY], + }) + ), + http.get('http://localhost:4466/apis/gateway.networking.k8s.io/v1beta1/gateways', () => + HttpResponse.error() + ), + ], + }, + }, + }, +} as Meta; + +const Template: StoryFn = () => { + return ; +}; + +export const Items = Template.bind({}); diff --git a/frontend/src/components/gateway/GatewayList.tsx b/frontend/src/components/gateway/GatewayList.tsx new file mode 100644 index 00000000000..94fb4d1ee99 --- /dev/null +++ b/frontend/src/components/gateway/GatewayList.tsx @@ -0,0 +1,47 @@ +import { useTranslation } from 'react-i18next'; +import Gateway from '../../lib/k8s/gateway'; +import Link from '../common/Link'; +import ResourceListView from '../common/Resource/ResourceListView'; +import { makeGatewayStatusLabel } from './ClassList'; + +export default function GatewayList() { + const { t } = useTranslation(['glossary', 'translation']); + + return ( + gateway.spec?.gatewayClassName, + render: gateway => + gateway.spec?.gatewayClassName ? ( + + {gateway.spec?.gatewayClassName} + + ) : null, + }, + { + id: 'conditions', + label: t('translation|Conditions'), + getValue: (gateway: Gateway) => + gateway.status?.conditions?.find( + ({ status }: { status: string }) => status === 'True' + ) ?? null, + render: (gateway: Gateway) => makeGatewayStatusLabel(gateway.status?.conditions), + }, + { + id: 'listeners', + label: t('translation|Listeners'), + getValue: (gateway: Gateway) => gateway.spec.listeners.length, + }, + 'age', + ]} + /> + ); +} diff --git a/frontend/src/components/gateway/HTTPRouteDetails.tsx b/frontend/src/components/gateway/HTTPRouteDetails.tsx new file mode 100644 index 00000000000..4967cb07ab2 --- /dev/null +++ b/frontend/src/components/gateway/HTTPRouteDetails.tsx @@ -0,0 +1,112 @@ +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { GatewayParentReference } from '../../lib/k8s/gateway'; +import HTTPRoute, { HTTPRouteRule } from '../../lib/k8s/httpRoute'; +import { EmptyContent, LabelListItem, Link, NameValueTable, SimpleTable } from '../common'; +import { DetailsGrid } from '../common/Resource'; +import SectionBox from '../common/SectionBox'; + +function HTTPRouteRuleTable(props: { rule: HTTPRouteRule }) { + const { rule } = props; + const { t } = useTranslation(['glossary', 'translation']); + + const mainRows = [ + { + name: t('translation|BackendRefs'), + value: rule.backendRefs?.length, + hide: (rule.backendRefs?.length || 0) === 0, + }, + { + name: t('translation|Matches'), + value: rule.matches?.length, + hide: (rule.matches?.length || 0) === 0, + }, + ]; + return ; +} + +export default function HTTPRouteDetails(props: { name?: string; namespace?: string }) { + const params = useParams<{ namespace: string; name: string }>(); + const { name = params.name, namespace = params.namespace } = props; + const { t } = useTranslation(['glossary', 'translation']); + + return ( + + httpRoute && [ + { + name: 'Hostnames', + value: `${tls}`)} />, + }, + ] + } + withEvents + extraSections={(item: HTTPRoute) => + item && [ + { + id: 'headlamp.httproute-rules', + section: item && ( + + {item.rules.length === 0 ? ( + {t('No data in this config map')} + ) : ( + item.rules.map((rule: HTTPRouteRule, index: any) => ( + + )) + )} + + ), + }, + { + id: 'headlamp.httproute-parentrefs', + section: ( + + ( + + {data.name} + + ), + }, + { + label: t('translation|Namespace'), + getter: (data: GatewayParentReference) => data.namespace, + }, + { + label: t('translation|Kind'), + getter: (data: GatewayParentReference) => data.kind, + }, + { + label: t('translation|Group'), + getter: (data: GatewayParentReference) => data.group, + }, + { + label: t('translation|Section Name'), + getter: (data: GatewayParentReference) => data.sectionName, + }, + ]} + data={item?.parentRefs || []} + reflectInURL="listeners" + /> + + ), + }, + ] + } + /> + ); +} diff --git a/frontend/src/components/gateway/HTTPRouteList.tsx b/frontend/src/components/gateway/HTTPRouteList.tsx new file mode 100644 index 00000000000..654c702ded4 --- /dev/null +++ b/frontend/src/components/gateway/HTTPRouteList.tsx @@ -0,0 +1,34 @@ +import { useTranslation } from 'react-i18next'; +import HTTPRoute from '../../lib/k8s/httpRoute'; +import { LabelListItem } from '../common'; +import ResourceListView from '../common/Resource/ResourceListView'; + +export default function HTTPRouteList() { + const { t } = useTranslation(['glossary', 'translation']); + + return ( + httpRoute.hostnames.join(''), + render: httpRoute => ( + host || '*')} /> + ), + }, + { + id: 'rules', + label: t('translation|rules'), + getValue: (httpRoute: HTTPRoute) => httpRoute.spec.rules.length, + }, + 'age', + ]} + /> + ); +} diff --git a/frontend/src/components/gateway/__snapshots__/ClassDetails.Basic.stories.storyshot b/frontend/src/components/gateway/__snapshots__/ClassDetails.Basic.stories.storyshot new file mode 100644 index 00000000000..c80c7aeabea --- /dev/null +++ b/frontend/src/components/gateway/__snapshots__/ClassDetails.Basic.stories.storyshot @@ -0,0 +1,288 @@ + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    + GatewayClass +

    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + Name +
    +
    + + resource-example-ingress + +
    +
    + Namespace +
    +
    + + default + +
    +
    + Creation +
    +
    + + 2023-07-19T09:48:42.000Z + +
    +
    + Controller Name +
    +
    + + test + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Conditions +

    +
    +
    +
    +
    +
    +
    +
    +

    + No data to be shown. +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Events +

    +
    +
    +
    +
    +
    +
    +
    +

    + No data to be shown. +

    +
    +
    +
    +
    +
    +
    +
    +
    + \ No newline at end of file diff --git a/frontend/src/components/gateway/__snapshots__/ClassList.Items.stories.storyshot b/frontend/src/components/gateway/__snapshots__/ClassList.Items.stories.storyshot new file mode 100644 index 00000000000..9c53feb9cb0 --- /dev/null +++ b/frontend/src/components/gateway/__snapshots__/ClassList.Items.stories.storyshot @@ -0,0 +1,707 @@ + +
    +
    +
    +
    +
    +

    + Gateway Classes +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    + + + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    + + resource-example-ingress + + + test + + +

    + 3mo +

    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    + + 1-1 of 1 + +
    + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + \ No newline at end of file diff --git a/frontend/src/components/gateway/__snapshots__/GatewayDetails.Basic.stories.storyshot b/frontend/src/components/gateway/__snapshots__/GatewayDetails.Basic.stories.storyshot new file mode 100644 index 00000000000..7bdef5e127f --- /dev/null +++ b/frontend/src/components/gateway/__snapshots__/GatewayDetails.Basic.stories.storyshot @@ -0,0 +1,383 @@ + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    + Gateway +

    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + Name +
    +
    + + tls-example-ingress + +
    +
    + Namespace +
    +
    + + default + +
    +
    + Creation +
    +
    + + 2023-07-19T09:48:42.000Z + +
    +
    + Class Name +
    +
    + + test + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Addresses +

    +
    +
    +
    +
    +
    +
    +
    +

    + No addresses data to be shown. +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Listeners +

    +
    +
    +
    +
    +
    +
    +

    + No data in this config map +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Conditions +

    +
    +
    +
    +
    +
    +
    +
    +

    + No data to be shown. +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Events +

    +
    +
    +
    +
    +
    +
    +
    +

    + No data to be shown. +

    +
    +
    +
    +
    +
    +
    +
    +
    + \ No newline at end of file diff --git a/frontend/src/components/gateway/__snapshots__/GatewayList.Items.stories.storyshot b/frontend/src/components/gateway/__snapshots__/GatewayList.Items.stories.storyshot new file mode 100644 index 00000000000..82683c6cc3c --- /dev/null +++ b/frontend/src/components/gateway/__snapshots__/GatewayList.Items.stories.storyshot @@ -0,0 +1,976 @@ + +
    +
    +
    +
    +
    +

    + Gateways +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    + + + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    + + tls-example-ingress + + + + default + + + + test + + + + 0 + +

    + 3mo +

    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    + + 1-1 of 1 + +
    + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + \ No newline at end of file diff --git a/frontend/src/components/gateway/storyHelper.ts b/frontend/src/components/gateway/storyHelper.ts new file mode 100644 index 00000000000..e1e9925a1a1 --- /dev/null +++ b/frontend/src/components/gateway/storyHelper.ts @@ -0,0 +1,38 @@ +import { KubeGateway } from '../../lib/k8s/gateway'; + +export const DEFAULT_GATEWAY: KubeGateway = { + apiVersion: 'gateway.networking.k8s.io/v1beta1', + kind: 'Gateway', + metadata: { + creationTimestamp: '2023-07-19T09:48:42Z', + generation: 1, + name: 'tls-example-ingress', + namespace: 'default', + resourceVersion: '12345', + uid: 'abc123', + }, + spec: { + gatewayClassName: 'test', + listeners: [], + }, + status: { + addresses: [], + listeners: [], + }, +}; + +export const RESOURCE_GATEWAY_CLASS = { + apiVersion: 'gateway.networking.k8s.io/v1beta1', + kind: 'GatewayClass', + metadata: { + creationTimestamp: '2023-07-19T09:48:42Z', + generation: 1, + name: 'resource-example-ingress', + namespace: 'default', + resourceVersion: '1234', + uid: 'abc1234', + }, + spec: { + controllerName: 'test', + }, +}; diff --git a/frontend/src/lib/k8s/gateway.ts b/frontend/src/lib/k8s/gateway.ts new file mode 100644 index 00000000000..b2ede58dbfa --- /dev/null +++ b/frontend/src/lib/k8s/gateway.ts @@ -0,0 +1,79 @@ +import { KubeCondition } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; + +export interface GatewayParentReference { + group: string; + kind: string; + namespace: string; + sectionName: string | null; + name: string; + [key: string]: any; +} + +export interface GatewayListener { + hostname: string; + name: string; + protocol: string; + [key: string]: any; +} +export interface GatewayListenerStatus { + name: string; + conditions: KubeCondition[]; + [key: string]: any; +} +export interface GatewayAddress { + type: string; + value: string; +} + +export interface KubeGateway extends KubeObjectInterface { + spec: { + gatewayClassName?: string; + listeners: GatewayListener[]; + [key: string]: any; + }; + status: { + addresses: GatewayAddress[]; + listeners: GatewayListenerStatus[]; + [otherProps: string]: any; + }; +} + +class Gateway extends KubeObject { + static kind = 'Gateway'; + static apiName = 'gateways'; + static apiVersion = 'gateway.networking.k8s.io/v1beta1'; + static isNamespaced = true; + + get spec(): KubeGateway['spec'] { + return this.jsonData.spec; + } + + get status() { + return this.jsonData.status; + } + + getListeners(): GatewayListener[] { + return this.jsonData.spec.listeners; + } + + getAddresses(): GatewayAddress[] { + return this.jsonData.status.addresses; + } + + getListernerStatusByName(name: string): GatewayListenerStatus | null { + return this.jsonData.status.listeners.find(t => t.name === name) || null; + } + + static get pluralName() { + return 'gateways'; + } + get listRoute(): string { + return 'k8sgateways'; // fix magic name gateway + } + get detailsRoute(): string { + return 'k8sgateway'; // fix magic name gateway + } +} + +export default Gateway; diff --git a/frontend/src/lib/k8s/gatewayClass.ts b/frontend/src/lib/k8s/gatewayClass.ts new file mode 100644 index 00000000000..f8822357086 --- /dev/null +++ b/frontend/src/lib/k8s/gatewayClass.ts @@ -0,0 +1,40 @@ +import { KubeObject, KubeObjectInterface } from './KubeObject'; + +export interface KubeGatewayClass extends KubeObjectInterface { + spec: { + controllerName: string; + [key: string]: any; + }; + status: { + [otherProps: string]: any; + }; +} + +class GatewayClass extends KubeObject { + static kind = 'GatewayClass'; + static apiName = 'gatewayclasses'; + static apiVersion = 'gateway.networking.k8s.io/v1beta1'; + static isNamespaced = false; + + get spec(): KubeGatewayClass['spec'] { + return this.jsonData.spec; + } + + get status() { + return this.jsonData.status; + } + + get controllerName() { + return this.spec!.controllerName; + } + + static get listRoute() { + return 'GatewayClasses'; + } + + static get pluralName() { + return 'GatewayClasses'; + } +} + +export default GatewayClass; diff --git a/frontend/src/lib/k8s/grpcRoute.ts b/frontend/src/lib/k8s/grpcRoute.ts new file mode 100644 index 00000000000..fd6d5413804 --- /dev/null +++ b/frontend/src/lib/k8s/grpcRoute.ts @@ -0,0 +1,29 @@ +import { GatewayParentReference } from './gateway'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; + +export interface KubeGRPCRoute extends KubeObjectInterface { + spec: { + parentRefs: GatewayParentReference[]; + [key: string]: any; + }; +} + +class GRPCRoute extends KubeObject { + static kind = 'GRPCRoute'; + static apiName = 'grpcroutes'; + static apiVersion = 'gateway.networking.k8s.io/v1beta1'; + static isNamespaced = true; + + get spec(): KubeGRPCRoute['spec'] { + return this.jsonData.spec; + } + get parentRefs(): GatewayParentReference[] { + return this.jsonData.spec.parentRefs; + } + + static get pluralName() { + return 'grpcroutes'; + } +} + +export default GRPCRoute; diff --git a/frontend/src/lib/k8s/httpRoute.ts b/frontend/src/lib/k8s/httpRoute.ts new file mode 100644 index 00000000000..ade384acb01 --- /dev/null +++ b/frontend/src/lib/k8s/httpRoute.ts @@ -0,0 +1,46 @@ +import { GatewayParentReference } from './gateway'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; + +export interface HTTPRouteRule { + backendRefs: any[] | null; + matches: any[] | null; + [key: string]: any; +} + +export interface KubeHTTPRoute extends KubeObjectInterface { + spec: { + hostnames: string[]; + parentRefs: GatewayParentReference[]; + rules: HTTPRouteRule[]; + [key: string]: any; + }; +} + +class HTTPRoute extends KubeObject { + static kind = 'HTTPRoute'; + static apiName = 'httproutes'; + static apiVersion = 'gateway.networking.k8s.io/v1beta1'; + static isNamespaced = true; + + get spec(): KubeHTTPRoute['spec'] { + return this.jsonData.spec; + } + + get hostnames(): string[] { + return this.jsonData.spec.hostnames; + } + + get rules(): HTTPRouteRule[] { + return this.jsonData.spec.rules; + } + + get parentRefs(): GatewayParentReference[] { + return this.jsonData.spec.parentRefs; + } + + static get pluralName() { + return 'httproutes'; + } +} + +export default HTTPRoute; diff --git a/frontend/src/lib/router.tsx b/frontend/src/lib/router.tsx index dd533784b25..47fcacdc190 100644 --- a/frontend/src/lib/router.tsx +++ b/frontend/src/lib/router.tsx @@ -27,6 +27,14 @@ import DaemonSetList from '../components/daemonset/List'; import DeploymentsList from '../components/deployments/List'; import EndpointDetails from '../components/endpoints/Details'; import EndpointList from '../components/endpoints/List'; +import GatewayClassDetails from '../components/gateway/ClassDetails'; +import GatewayClassList from '../components/gateway/ClassList'; +import GatewayDetails from '../components/gateway/GatewayDetails'; +import GatewayList from '../components/gateway/GatewayList'; +import GRPCRouteDetails from '../components/gateway/GRPCRouteDetails'; +import GRPCRouteList from '../components/gateway/GRPCRouteList'; +import HTTPRouteDetails from '../components/gateway/HTTPRouteDetails'; +import HTTPRouteList from '../components/gateway/HTTPRouteList'; import HpaDetails from '../components/horizontalPodAutoscaler/Details'; import HpaList from '../components/horizontalPodAutoscaler/List'; import IngressClassDetails from '../components/ingress/ClassDetails'; @@ -327,6 +335,63 @@ const defaultRoutes: { sidebar: 'NetworkPolicies', component: () => , }, + k8sgateways: { + // fix magic name gateway + path: '/k8sgateways', + exact: true, + name: 'Gateways', + sidebar: 'k8sgateways', + component: () => , + }, + k8sgateway: { + // fix magic name gateway + path: '/k8sgateways/:namespace/:name', + exact: true, + name: 'Gateways', + sidebar: 'k8sgateways', + component: () => , + }, + httproutes: { + path: '/httproutes', + exact: true, + name: 'HttpRoutes', + sidebar: 'httproutes', + component: () => , + }, + httproute: { + path: '/httproutes/:namespace/:name', + exact: true, + name: 'HttpRoutes', + sidebar: 'httproutes', + component: () => , + }, + grpcroutes: { + path: '/grpcroutes', + exact: true, + name: 'GRPCRoutes', + sidebar: 'grpcroutes', + component: () => , + }, + grpcroute: { + path: '/grpcroutes/:namespace/:name', + exact: true, + name: 'GRPCRoutes', + sidebar: 'grpcroutes', + component: () => , + }, + gatewayclasses: { + path: '/gatewayclasses', + exact: true, + name: 'GatewayClasses', + sidebar: 'gatewayclasses', + component: () => , + }, + gatewayclass: { + path: '/gatewayclasses/:name', + exact: true, + sidebar: 'gatewayclasses', + component: () => , + }, DaemonSets: { path: '/daemonsets', exact: true,