From c91bcc3bc13b274312de3d0656d8ea865a3af27b Mon Sep 17 00:00:00 2001 From: Alberto Gutierrez Date: Wed, 7 Feb 2024 18:08:00 +0100 Subject: [PATCH] feat(kiali): add KialiPage Component (#1180) * feat(kiali): add KialiPage Component * Set grid in overview cards for other namespaces --- plugins/kiali/DEVELOPMENT.md | 6 +- plugins/kiali/README.md | 42 +++++- plugins/kiali/app-config.janus-idp.yaml | 22 +++ plugins/kiali/dev/index.tsx | 59 ++++++-- plugins/kiali/package.json | 1 + plugins/kiali/src/Router.tsx | 97 +++++++------ .../src/components/About/AboutUIModal.tsx | 8 +- .../DebugInformation/DebugInformation.tsx | 2 +- .../MessageCenter/MessageCenter.tsx | 4 +- plugins/kiali/src/index.ts | 4 +- .../kiali/src/pages/Kiali/Header/Header.tsx | 130 ------------------ .../src/pages/Kiali/Header/HelpKiali.tsx | 60 ++++++++ .../src/pages/Kiali/Header/KialiHeader.tsx | 60 ++++++++ .../pages/Kiali/Header/KialiHeaderEntity.tsx | 51 +++++++ .../pages/Kiali/Header/NamespaceSelector.tsx | 76 ++++++++++ plugins/kiali/src/pages/Kiali/KialiEntity.tsx | 15 ++ plugins/kiali/src/pages/Kiali/KialiPage.tsx | 4 +- .../Overview/OverviewCard/OverviewCard.tsx | 43 +++--- .../kiali/src/pages/Overview/OverviewPage.tsx | 114 +++++++++------ plugins/kiali/src/plugin.ts | 4 +- plugins/kiali/src/store/KialiProvider.tsx | 12 +- plugins/kiali/src/utils/entityFilter.ts | 3 +- 22 files changed, 555 insertions(+), 262 deletions(-) create mode 100644 plugins/kiali/app-config.janus-idp.yaml delete mode 100644 plugins/kiali/src/pages/Kiali/Header/Header.tsx create mode 100644 plugins/kiali/src/pages/Kiali/Header/HelpKiali.tsx create mode 100644 plugins/kiali/src/pages/Kiali/Header/KialiHeader.tsx create mode 100644 plugins/kiali/src/pages/Kiali/Header/KialiHeaderEntity.tsx create mode 100644 plugins/kiali/src/pages/Kiali/Header/NamespaceSelector.tsx create mode 100644 plugins/kiali/src/pages/Kiali/KialiEntity.tsx diff --git a/plugins/kiali/DEVELOPMENT.md b/plugins/kiali/DEVELOPMENT.md index 7b8bd57bf5..9f15d796ef 100644 --- a/plugins/kiali/DEVELOPMENT.md +++ b/plugins/kiali/DEVELOPMENT.md @@ -79,7 +79,7 @@ async function main() { apiRouter.use('/kiali', await kiali(kialiEnv)); ``` -6. Configure you `app-config.yaml` with kiali configuration +6. Configure you `app-config.local.yaml` with kiali configuration ```yaml catalog: @@ -103,7 +103,7 @@ catalog: 7. Add catalog -Add to locations in `app-config.yaml` +Add to locations in `app-config.local.yaml` ```yaml locations: @@ -116,7 +116,7 @@ locations: ### Token authentication -1. Set the parameters in app-config.yaml +1. Set the parameters in app-config.local.yaml ```yaml catalog: diff --git a/plugins/kiali/README.md b/plugins/kiali/README.md index b1148c5795..91ca9c54c2 100644 --- a/plugins/kiali/README.md +++ b/plugins/kiali/README.md @@ -72,7 +72,7 @@ The Kiali plugin has the following capabilities: *** -#### Procedure +#### Setting up the OCM frontend package 1. Install the Kiali plugin using the following commands: @@ -80,7 +80,45 @@ The Kiali plugin has the following capabilities: yarn workspace app add @janus-idp/backstage-plugin-kiali ``` -2. Enable the **Kiali** tab on the entity view page using the `packages/app/src/components/catalog/EntityPage.tsx` file: +2. Select the components that you want to use, such as: + + - `KialiPage`: This is a standalone page or dashboard displaying all namespaces in the mesh. You can add `KialiPage` to `packages/app/src/App.tsx` file as follows: + + ```tsx title="packages/app/src/App.tsx" + /* highlight-add-next-line */ + import { KialiPage } from '@janus-idp/backstage-plugin-kiali'; + + const routes = ( + + {/* ... */} + {/* highlight-add-next-line */} + } /> + + ); + ``` + + You can also update navigation in `packages/app/src/components/Root/Root.tsx` as follows: + + ```tsx title="packages/app/src/components/Root/Root.tsx" + /* highlight-add-next-line */ + import { KialiIcon } from '@janus-idp/backstage-plugin-kiali'; + + export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + }> + {/* ... */} + {/* highlight-add-next-line */} + + + {/* ... */} + + {children} + + ); + ``` + + - `EntityKialiContent`: This component is a React context provided for Kiali data, which is related to the current entity. The `EntityKialiContent` component is used to display any data on the React components mentioned in `packages/app/src/components/catalog/EntityPage.tsx`: ```tsx title="packages/app/src/components/catalog/EntityPage.tsx" /* highlight-add-next-line */ diff --git a/plugins/kiali/app-config.janus-idp.yaml b/plugins/kiali/app-config.janus-idp.yaml new file mode 100644 index 0000000000..20572383ed --- /dev/null +++ b/plugins/kiali/app-config.janus-idp.yaml @@ -0,0 +1,22 @@ +dynamicPlugins: + frontend: + janus-idp.backstage-plugin-kiali: + appIcons: + - name: kialiIcon + importName: KialiIcon + dynamicRoutes: + - path: /kiali + importName: KialiPage + menuItem: + icon: kialiIcon + text: Kiali + mountPoints: + - config: + layout: + gridColumn: '1 / -1' + height: 75vh + if: + anyOf: + - hasAnnotation: backstage.io/kubernetes-namespace + importName: EntityKialiContent + mountPoint: entity.page.kiali/cards diff --git a/plugins/kiali/dev/index.tsx b/plugins/kiali/dev/index.tsx index e12f04e029..06df658f98 100644 --- a/plugins/kiali/dev/index.tsx +++ b/plugins/kiali/dev/index.tsx @@ -1,11 +1,16 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import { Content, Page } from '@backstage/core-components'; import { createDevApp } from '@backstage/dev-utils'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TestApiProvider } from '@backstage/test-utils'; -import { KialiPage, kialiPlugin } from '../src/plugin'; +import { KialiHeader } from '../src/pages/Kiali/Header/KialiHeader'; +import { KialiHeaderEntity } from '../src/pages/Kiali/Header/KialiHeaderEntity'; +import { KialiEntity } from '../src/pages/Kiali/KialiEntity'; +import { OverviewPage } from '../src/pages/Overview/OverviewPage'; +import { kialiPlugin } from '../src/plugin'; import { KialiApi, kialiApiRef } from '../src/services/Api'; import { KialiProvider } from '../src/store/KialiProvider'; import { AuthInfo } from '../src/types/Auth'; @@ -237,24 +242,54 @@ class MockKialiClient implements KialiApi { return true; } } -// @ts-expect-error -const MockProvider = ({ children }) => ( - - - {children} - - -); + +interface Props { + children: React.ReactNode; + isEntity?: boolean; +} + +const MockProvider = (props: Props) => { + const content = ( + + + {!props.isEntity && } + + {props.isEntity && } + {props.children} + + + + ); + + const viewIfEntity = props.isEntity && ( + {content} + ); + + return ( + + {viewIfEntity || content} + + ); +}; createDevApp() .registerPlugin(kialiPlugin) .addPage({ element: ( - + + + ), + title: 'Kiali Overview', + path: '/overview', + }) + .addPage({ + element: ( + + ), - title: 'Overview Page', - path: '/kiali/overview', + title: 'Kiali Entity', + path: '/kiali', }) .render(); diff --git a/plugins/kiali/package.json b/plugins/kiali/package.json index 22e3aa5b33..3c000f7cd7 100644 --- a/plugins/kiali/package.json +++ b/plugins/kiali/package.json @@ -34,6 +34,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.11.3", "@material-ui/lab": "^4.0.0-alpha.45", + "@mui/icons-material": "^5.15.8", "@patternfly/patternfly": "^5.1.0", "@patternfly/react-charts": "^7.1.1", "@patternfly/react-core": "^5.1.1", diff --git a/plugins/kiali/src/Router.tsx b/plugins/kiali/src/Router.tsx index a9e238f3fb..dfbd71a36a 100644 --- a/plugins/kiali/src/Router.tsx +++ b/plugins/kiali/src/Router.tsx @@ -2,63 +2,82 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; import { Entity } from '@backstage/catalog-model'; -import { MissingAnnotationEmptyState } from '@backstage/core-components'; +import { + Content, + MissingAnnotationEmptyState, + Page, +} from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; import { Button } from '@material-ui/core'; -import { KialiNoPath, KialiPage } from './pages/Kiali'; +import { KialiNoPath } from './pages/Kiali'; +import { KialiHeader } from './pages/Kiali/Header/KialiHeader'; +import { KialiHeaderEntity } from './pages/Kiali/Header/KialiHeaderEntity'; +import { KialiEntity } from './pages/Kiali/KialiEntity'; +import { OverviewPage } from './pages/Overview/OverviewPage'; import { KialiProvider } from './store/KialiProvider'; export const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes-id'; export const KUBERNETES_NAMESPACE = 'backstage.io/kubernetes-namespace'; - export const KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION = 'backstage.io/kubernetes-label-selector'; -export const isKubernetesAvailable = (entity: Entity) => - Boolean(entity.metadata.annotations?.[KUBERNETES_ANNOTATION]) || - Boolean( - entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION], +const validateAnnotation = (entity: Entity) => { + return ( + Boolean(entity.metadata.annotations?.[KUBERNETES_NAMESPACE]) || + Boolean(entity.metadata.annotations?.[KUBERNETES_ANNOTATION]) || + Boolean( + entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION], + ) ); +}; -export const Router = () => { - const { entity } = useEntity(); - const kubernetesAnnotationValue = - entity.metadata.annotations?.[KUBERNETES_ANNOTATION]; - - const kubernetesNamespaceValue = - entity.metadata.annotations?.[KUBERNETES_NAMESPACE]; - - const kubernetesLabelSelectorQueryAnnotationValue = - entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION]; +/* + Router for entity +*/ - if ( - kubernetesAnnotationValue || - kubernetesNamespaceValue || - kubernetesLabelSelectorQueryAnnotationValue - ) { +export const EmbeddedRouter = () => { + const { entity } = useEntity(); + if (!validateAnnotation(entity)) { return ( - - - } /> - } /> - } /> - - + <> + +

+ Or use a label selector query, which takes precedence over the + previous annotation. +

+ + ); } return ( - <> - -

- Or use a label selector query, which takes precedence over the previous - annotation. -

- - + + + + } /> + } /> + + + ); +}; + +export const Router = () => { + return ( + + + + + + } /> + } /> + } /> + + + + ); }; diff --git a/plugins/kiali/src/components/About/AboutUIModal.tsx b/plugins/kiali/src/components/About/AboutUIModal.tsx index 8515d72ad1..2fb50d2f76 100644 --- a/plugins/kiali/src/components/About/AboutUIModal.tsx +++ b/plugins/kiali/src/components/About/AboutUIModal.tsx @@ -82,14 +82,14 @@ export const AboutUIModal = (props: AboutUIModalProps) => { : `${externalService.name} URL`; const additionalInfo = additionalComponentInfoContent(externalService); return ( -
- + <> + {name} - + {additionalInfo} -
+ ); }; diff --git a/plugins/kiali/src/components/DebugInformation/DebugInformation.tsx b/plugins/kiali/src/components/DebugInformation/DebugInformation.tsx index 4690d94a40..815c111aa4 100644 --- a/plugins/kiali/src/components/DebugInformation/DebugInformation.tsx +++ b/plugins/kiali/src/components/DebugInformation/DebugInformation.tsx @@ -209,7 +209,7 @@ export const DebugInformation = (props: DebugInformationProps) => { ); const kialiConfigCard = ( - + {copyClip} ); diff --git a/plugins/kiali/src/components/MessageCenter/MessageCenter.tsx b/plugins/kiali/src/components/MessageCenter/MessageCenter.tsx index 6a66f8072a..61382c2fda 100644 --- a/plugins/kiali/src/components/MessageCenter/MessageCenter.tsx +++ b/plugins/kiali/src/components/MessageCenter/MessageCenter.tsx @@ -74,7 +74,7 @@ const calculateMessageStatus = (state: KialiAppState) => { ); }; -export const MessageCenter = () => { +export const MessageCenter = (props: { color?: string }) => { const kialiState = React.useContext(KialiContext) as KialiAppState; const [isOpen, toggleDrawer] = React.useState(false); const messageCenterStatus = calculateMessageStatus(kialiState); @@ -99,7 +99,7 @@ export const MessageCenter = () => { } color={messageCenterStatus.badgeDanger ? 'error' : 'primary'} > - + toggleDrawer(false)}> diff --git a/plugins/kiali/src/index.ts b/plugins/kiali/src/index.ts index 136f7ca3c5..524964a7a3 100644 --- a/plugins/kiali/src/index.ts +++ b/plugins/kiali/src/index.ts @@ -1 +1,3 @@ -export { kialiPlugin, EntityKialiContent } from './plugin'; +export { kialiPlugin, EntityKialiContent, KialiPage } from './plugin'; + +export { default as KialiIcon } from '@mui/icons-material/Troubleshoot'; diff --git a/plugins/kiali/src/pages/Kiali/Header/Header.tsx b/plugins/kiali/src/pages/Kiali/Header/Header.tsx deleted file mode 100644 index de070620a4..0000000000 --- a/plugins/kiali/src/pages/Kiali/Header/Header.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; - -import { ContentHeader, Select } from '@backstage/core-components'; - -import { Button, Chip, Grid, Menu, MenuItem, Tooltip } from '@material-ui/core'; -import { ClusterIcon } from '@patternfly/react-icons'; -import { QuestionCircleIcon } from '@patternfly/react-icons/'; - -import { NamespaceActions } from '../../../actions'; -import { AboutUIModal } from '../../../components/About/AboutUIModal'; -import { DebugInformation } from '../../../components/DebugInformation/DebugInformation'; -import { MessageCenter } from '../../../components/MessageCenter/MessageCenter'; -import { homeCluster } from '../../../config'; -import { KialiAppState, KialiContext } from '../../../store'; - -export const KialiHeader = (props: { title: string }) => { - const kialiState = React.useContext(KialiContext) as KialiAppState; - const [anchorEl, setAnchorEl] = React.useState(null); - const [showAbout, setShowAbout] = React.useState(false); - const [showDebug, setShowDebug] = React.useState(false); - const open = Boolean(anchorEl); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setShowAbout(false); - setAnchorEl(null); - }; - - const openAbout = () => { - setShowAbout(true); - setAnchorEl(null); - }; - - const openDebugInformation = () => { - // Using wrapped component, so we have to get the wrappedInstance - setShowDebug(true); - setAnchorEl(null); - }; - - const title = props.title.charAt(0).toUpperCase() + props.title.slice(1); - return ( -
- ns.name)} + multiple + onChange={handleChange} + renderValue={selected => (selected as string[]).join(', ')} + MenuProps={MenuProps} + style={{ color: props.page ? 'white' : undefined }} + > + {kialiState.namespaces.activeNamespaces.map(ns => ( + + activeNs.name) + .indexOf(ns.name) > -1 + } + /> + + + ))} + +
+ ); +}; diff --git a/plugins/kiali/src/pages/Kiali/KialiEntity.tsx b/plugins/kiali/src/pages/Kiali/KialiEntity.tsx new file mode 100644 index 0000000000..4d3e21f040 --- /dev/null +++ b/plugins/kiali/src/pages/Kiali/KialiEntity.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Content, Page } from '@backstage/core-components'; + +import { OverviewPage } from '../Overview/OverviewPage'; + +export const KialiEntity = () => { + return ( + + + + + + ); +}; diff --git a/plugins/kiali/src/pages/Kiali/KialiPage.tsx b/plugins/kiali/src/pages/Kiali/KialiPage.tsx index bb5a93fc8a..1834638add 100644 --- a/plugins/kiali/src/pages/Kiali/KialiPage.tsx +++ b/plugins/kiali/src/pages/Kiali/KialiPage.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Content, Page } from '@backstage/core-components'; import { OverviewPage } from '../Overview/OverviewPage'; -import { KialiHeader } from './Header/Header'; +import { KialiHeader } from './Header/KialiHeader'; import { KialiNoPath } from './NoPath'; const noPath = 'noPath'; @@ -32,7 +32,7 @@ export const KialiPage = () => { return ( - + {renderPath()} diff --git a/plugins/kiali/src/pages/Overview/OverviewCard/OverviewCard.tsx b/plugins/kiali/src/pages/Overview/OverviewCard/OverviewCard.tsx index bf632d0664..d64b104fe3 100644 --- a/plugins/kiali/src/pages/Overview/OverviewCard/OverviewCard.tsx +++ b/plugins/kiali/src/pages/Overview/OverviewCard/OverviewCard.tsx @@ -33,6 +33,7 @@ import { OverviewCardSparklineCharts } from './OverviewCardSparklineCharts'; type OverviewCardProps = { namespace: NamespaceInfo; + entity?: boolean; canaryUpgradeStatus?: CanaryUpgradeStatus; duration: DurationInSeconds; refreshInterval: IntervalInMilliseconds; @@ -111,9 +112,9 @@ export const OverviewCard = (props: OverviewCardProps) => { return ( - + {!props.entity && } - {isMultiCluster && props.namespace.cluster && ( + {!props.entity && isMultiCluster && props.namespace.cluster && ( <> {props.namespace.cluster} @@ -121,23 +122,27 @@ export const OverviewCard = (props: OverviewCardProps) => { )} - -
-
- Istio config -
- {props.namespace.tlsStatus && ( - - - - )} - {props.istioAPIEnabled - ? renderIstioConfigStatus(props.namespace) - : 'N/A'} -
- + {!props.entity && ( + <> + +
+
+ Istio config +
+ {props.namespace.tlsStatus && ( + + + + )} + {props.istioAPIEnabled + ? renderIstioConfigStatus(props.namespace) + : 'N/A'} +
+ + )} + {!props.entity && } {isIstioSystem && ( <> { +export const OverviewPage = (props: { entity?: boolean }) => { const kialiClient = useApi(kialiApiRef); - kialiClient.setEntity(useEntity().entity); const kialiState = React.useContext(KialiContext) as KialiAppState; const promises = new PromisesRegistry(); const [namespaces, setNamespaces] = React.useState([]); @@ -400,13 +398,15 @@ export const OverviewPage = () => { const isAscending = FilterHelper.isCurrentSortAscending(); const sortField = FilterHelper.currentSortField(Sorts.sortFields); const sortNs = sortedNamespaces(allNamespaces); - fetchHealth(sortNs, isAscending, sortField); - fetchTLS(sortNs, isAscending, sortField); - fetchValidations(sortNs, isAscending, sortField); + if (!props.entity) { + fetchHealth(sortNs, isAscending, sortField); + fetchTLS(sortNs, isAscending, sortField); + fetchValidations(sortNs, isAscending, sortField); + fetchOutboundTrafficPolicyMode(); + fetchCanariesStatus(); + fetchIstiodResourceThresholds(); + } fetchMetrics(sortNs); - fetchOutboundTrafficPolicyMode(); - fetchCanariesStatus(); - fetchIstiodResourceThresholds(); promises.waitAll(); setNamespaces(sortNs); }); @@ -421,40 +421,74 @@ export const OverviewPage = () => { return ; } + const overviewLinkInfo = { title: 'Go to Full Overview', link: '#' }; + return ( - load()} - overviewType={overviewType} - setOverviewType={setOverviewType} - directionType={directionType} - setDirectionType={setDirectionType} - duration={duration} - setDuration={setDuration} - /> - - {filterActiveNamespaces().map((ns, i) => ( - - + {props.entity ? ( + + {filterActiveNamespaces().map(ns => ( + + + + ))} + + ) : ( + <> + load()} + overviewType={overviewType} + setOverviewType={setOverviewType} + directionType={directionType} + setDirectionType={setDirectionType} + duration={duration} + setDuration={setDuration} + /> + + {filterActiveNamespaces().map((ns, i) => ( + + + + ))} - ))} - + + )} ); diff --git a/plugins/kiali/src/plugin.ts b/plugins/kiali/src/plugin.ts index 494c2444a9..0faaf576bd 100644 --- a/plugins/kiali/src/plugin.ts +++ b/plugins/kiali/src/plugin.ts @@ -32,7 +32,7 @@ export const kialiPlugin = createPlugin({ export const KialiPage = kialiPlugin.provide( createRoutableExtension({ name: 'KialiPage', - component: () => import('./pages/Kiali/KialiPage').then(m => m.KialiPage), + component: () => import('./Router').then(m => m.Router), mountPoint: rootRouteRef, }), ); @@ -54,7 +54,7 @@ export const EntityKialiContent: ( ) => JSX.Element = kialiPlugin.provide( createRoutableExtension({ name: 'EntityKialiContent', - component: () => import('./Router').then(m => m.Router), + component: () => import('./Router').then(m => m.EmbeddedRouter), mountPoint: rootRouteRef, }), ); diff --git a/plugins/kiali/src/store/KialiProvider.tsx b/plugins/kiali/src/store/KialiProvider.tsx index 6babd10d1c..902065f6ae 100644 --- a/plugins/kiali/src/store/KialiProvider.tsx +++ b/plugins/kiali/src/store/KialiProvider.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { useAsyncFn, useDebounce } from 'react-use'; +import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; -import { useEntity } from '@backstage/plugin-catalog-react'; import { CircularProgress } from '@material-ui/core'; import axios from 'axios'; @@ -49,12 +49,17 @@ const initialChecker: KialiChecker = { interface Props { children: React.ReactNode; + entity?: Entity; } -export const KialiProvider: React.FC = ({ children }): JSX.Element => { +export const KialiProvider: React.FC = ({ + children, + entity, +}): JSX.Element => { const promises = new PromisesRegistry(); const [kialiCheck, setKialiCheck] = React.useState(initialChecker); + const [loginState, loginDispatch] = React.useReducer( LoginReducer, initialStore.authentication, @@ -87,8 +92,9 @@ export const KialiProvider: React.FC = ({ children }): JSX.Element => { IstioCertsInfoStateReducer, initialStore.istioCertsInfo, ); + const kialiClient = useApi(kialiApiRef); - kialiClient.setEntity(useEntity().entity); + kialiClient.setEntity(entity); const alertUtils = new AlertUtils(messageDispatch); const fetchNamespaces = async () => { diff --git a/plugins/kiali/src/utils/entityFilter.ts b/plugins/kiali/src/utils/entityFilter.ts index ddd3d2968b..da17b9aa41 100644 --- a/plugins/kiali/src/utils/entityFilter.ts +++ b/plugins/kiali/src/utils/entityFilter.ts @@ -33,9 +33,8 @@ const filterByNs = (ns: Namespace[], value: string): Namespace[] => { export const filterNsByAnnotation = ( ns: Namespace[], entity: Entity | undefined, - global: boolean = false, ): Namespace[] => { - if (global || !entity) { + if (!entity) { return ns; } const annotations = entity?.metadata?.annotations || undefined;