diff --git a/plugins/kiali/dev/index.tsx b/plugins/kiali/dev/index.tsx index b7ea2ff15f..4a1f078268 100644 --- a/plugins/kiali/dev/index.tsx +++ b/plugins/kiali/dev/index.tsx @@ -14,7 +14,7 @@ import { TestApiProvider } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; -import { kialiPlugin } from '../src'; +import { EntityKialiResourcesCard, kialiPlugin } from '../src'; import { getEntityRoutes, getRoutes } from '../src/components/Router'; import { KialiHeader } from '../src/pages/Kiali/Header/KialiHeader'; import { KialiHelper } from '../src/pages/Kiali/KialiHelper'; @@ -530,6 +530,30 @@ const MockProvider = (props: Props) => { ); }; +const MockEntityCard = () => { + const content = ( + + +
+ + + + + + + +
+
+
+ ); + + return ( + + {content} + + ); +}; + const MockKialiError = () => { const errorsTypes: KialiChecker[] = [ { @@ -619,4 +643,9 @@ createDevApp() title: 'No Annotation', path: '/no-annotation', }) + .addPage({ + element: , + title: 'Resources card', + path: '/kiali-entity-card', + }) .render(); diff --git a/plugins/kiali/src/components/DetailDescription/DetailDescription.tsx b/plugins/kiali/src/components/DetailDescription/DetailDescription.tsx index 6fb0e311c1..143286f285 100644 --- a/plugins/kiali/src/components/DetailDescription/DetailDescription.tsx +++ b/plugins/kiali/src/components/DetailDescription/DetailDescription.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { Link } from 'react-router-dom'; import { Tooltip } from '@material-ui/core'; @@ -10,6 +11,7 @@ import { kialiStyle } from '../../styles/StyleUtils'; import { AppWorkload } from '../../types/App'; import * as H from '../../types/Health'; import { HealthSubItem } from '../../types/Health'; +import { DRAWER } from '../../types/types'; import { Workload } from '../../types/Workload'; import { JanusObjectLink } from '../../utils/janusLinks'; import { renderTrafficStatus } from '../Health/HealthDetails'; @@ -25,6 +27,7 @@ type Props = { services?: string[]; waypointWorkloads?: Workload[]; workloads?: AppWorkload[]; + view?: string; }; const iconStyle = kialiStyle({ @@ -74,7 +77,9 @@ export const DetailDescription: React.FC = (props: Props) => { namespace: string, appName: string, ): React.ReactNode => { - const link = ( + let link: React.ReactNode; + + link = ( = (props: Props) => { ); + let href = `/namespaces/${namespace}/applications/${appName}`; + + if (props.cluster && isMultiCluster) { + href = `${href}?clusterName=${props.cluster}`; + } + + if (props.view === DRAWER) { + href = `#application/${namespace}_${appName}`; + + link = {appName}; + } + return (
  • @@ -103,7 +120,9 @@ export const DetailDescription: React.FC = (props: Props) => { namespace: string, serviceName: string, ): React.ReactNode => { - const link = ( + let link: React.ReactNode; + + link = ( = (props: Props) => { ); + if (props.view === DRAWER) { + let href = `/namespaces/${namespace}/services/${serviceName}`; + + if (props.cluster && isMultiCluster) { + href = `${href}?clusterName=${props.cluster}`; + } + + href = `#service/${namespace}_${serviceName}`; + link = {serviceName}; + } + return (
  • @@ -184,7 +214,9 @@ export const DetailDescription: React.FC = (props: Props) => { }; const renderWorkloadItem = (workload: AppWorkload): React.ReactNode => { - const link = ( + let link: React.ReactNode; + + link = ( = (props: Props) => { {workload.workloadName} ); + + if (props.view === DRAWER) { + let href = `/namespaces/${props.namespace}/workloads/${workload.workloadName}`; + + if (props.cluster && isMultiCluster) { + href = `${href}?clusterName=${props.cluster}`; + } + + href = `#workload/${props.namespace}_${workload.workloadName}`; + link = {workload.workloadName}; + } + return (
    @@ -237,21 +281,34 @@ export const DetailDescription: React.FC = (props: Props) => { } if (workload) { - const link = ( - - {workload.workloadName} - - ); + let link: React.ReactNode; + + if (props.view === DRAWER) { + let href = `/namespaces/${props.namespace}/workloads/${workload.workloadName}`; + + if (props.cluster && isMultiCluster) { + href = `${href}?clusterName=${props.cluster}`; + } + + href = `#workload/${props.namespace}_${workload.workloadName}`; + link = {workload.workloadName}; + } else { + link = ( + + {workload.workloadName} + + ); + } return ( diff --git a/plugins/kiali/src/components/Drawers/AppDetailsDrawer.tsx b/plugins/kiali/src/components/Drawers/AppDetailsDrawer.tsx new file mode 100644 index 0000000000..6d8da201d3 --- /dev/null +++ b/plugins/kiali/src/components/Drawers/AppDetailsDrawer.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { useAsyncFn, useDebounce } from 'react-use'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { CircularProgress } from '@material-ui/core'; +import { AxiosError } from 'axios'; + +import { HistoryManager } from '../../app/History'; +import { AppInfo } from '../../pages/AppDetails/AppInfo'; +import { kialiApiRef } from '../../services/Api'; +import { App, AppQuery } from '../../types/App'; +import { AppHealth } from '../../types/Health'; +import { DRAWER } from '../../types/types'; + +type Props = { + namespace: string; + app: string; +}; +export const AppDetailsDrawer = (props: Props) => { + const kialiClient = useApi(kialiApiRef); + const [appItem, setAppItem] = React.useState(); + const [health, setHealth] = React.useState(); + const cluster = HistoryManager.getClusterName(); + + const fetchApp = async () => { + const params: AppQuery = { + rateInterval: `60s`, + health: 'true', + }; + + kialiClient + .getApp(props.namespace, props.app, params, cluster) + .then((appResponse: App) => { + const healthR = AppHealth.fromJson( + props.namespace, + props.app, + appResponse.health, + { + rateInterval: 60, + hasSidecar: appResponse.workloads.some(w => w.istioSidecar), + hasAmbient: appResponse.workloads.some(w => w.istioAmbient), + }, + ); + setAppItem(appResponse); + setHealth(healthR); + }) + .catch((err: AxiosError) => { + // eslint-disable-next-line no-console + console.log(err); + }); + }; + + const [{ loading }, refresh] = useAsyncFn( + async () => { + // Check if the config is loaded + await fetchApp(); + }, + [], + { loading: true }, + ); + useDebounce(refresh, 10); + + if (loading) { + return ; + } + + return ( + <> + {appItem && ( + + )} + + ); +}; diff --git a/plugins/kiali/src/components/Drawers/ServiceDetailsDrawer.tsx b/plugins/kiali/src/components/Drawers/ServiceDetailsDrawer.tsx new file mode 100644 index 0000000000..73afb11fb8 --- /dev/null +++ b/plugins/kiali/src/components/Drawers/ServiceDetailsDrawer.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { useAsyncFn, useDebounce } from 'react-use'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { CircularProgress } from '@material-ui/core'; + +import { HistoryManager } from '../../app/History'; +import { ServiceInfo } from '../../pages/ServiceDetails/ServiceInfo'; +import { kialiApiRef } from '../../services/Api'; +import { Validations } from '../../types/IstioObjects'; +import { ServiceDetailsInfo } from '../../types/ServiceInfo'; +import { DRAWER } from '../../types/types'; + +type Props = { + namespace: string; + service: string; +}; +export const ServiceDetailsDrawer = (props: Props) => { + const kialiClient = useApi(kialiApiRef); + const [serviceItem, setServiceItem] = React.useState(); + const cluster = HistoryManager.getClusterName(); + const [validations, setValidations] = React.useState({}); + + const fetchService = async () => { + kialiClient + .getServiceDetail(props.namespace, props.service, true, cluster, 60) + .then((serviceResponse: ServiceDetailsInfo) => { + setServiceItem(serviceResponse); + setValidations(serviceResponse.validations); + }) + .catch(err => { + // eslint-disable-next-line no-console + console.log(err); + }); + }; + + const [{ loading }, refresh] = useAsyncFn( + async () => { + // Check if the config is loaded + await fetchService(); + }, + [], + { loading: true }, + ); + useDebounce(refresh, 10); + + if (loading) { + return ; + } + + return ( + <> + {serviceItem && ( + + )} + + ); +}; diff --git a/plugins/kiali/src/components/Drawers/WorkloadDetailsDrawer.tsx b/plugins/kiali/src/components/Drawers/WorkloadDetailsDrawer.tsx new file mode 100644 index 0000000000..c2b09b767d --- /dev/null +++ b/plugins/kiali/src/components/Drawers/WorkloadDetailsDrawer.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { useAsyncFn, useDebounce } from 'react-use'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { CircularProgress } from '@material-ui/core'; + +import { WorkloadInfo } from '../../pages/WorkloadDetails/WorkloadInfo'; +import { kialiApiRef } from '../../services/Api'; +import { WorkloadHealth } from '../../types/Health'; +import { DRAWER } from '../../types/types'; +import { Workload, WorkloadQuery } from '../../types/Workload'; + +type Props = { + namespace: string; + workload: string; +}; +export const WorkloadDetailsDrawer = (props: Props) => { + const kialiClient = useApi(kialiApiRef); + const [workloadItem, setWorkloadItem] = React.useState(); + const [health, setHealth] = React.useState(); + + const fetchWorkload = async () => { + const query: WorkloadQuery = { + health: 'true', + rateInterval: `60s`, + validate: 'false', + }; + + kialiClient + .getWorkload( + props.namespace ? props.namespace : '', + props.workload ? props.workload : '', + query, + ) + .then((workloadResponse: Workload) => { + setWorkloadItem(workloadResponse); + + const wkHealth = WorkloadHealth.fromJson( + props.namespace ? props.namespace : '', + workloadResponse.name, + workloadResponse.health, + { + rateInterval: 60, + hasSidecar: workloadResponse.istioSidecar, + hasAmbient: workloadResponse.istioAmbient, + }, + ); + setHealth(wkHealth); + }) + .catch(err => { + // eslint-disable-next-line no-console + console.log(err); + }); + }; + + const [{ loading }, refresh] = useAsyncFn( + async () => { + // Check if the config is loaded + await fetchWorkload(); + }, + [], + { loading: true }, + ); + useDebounce(refresh, 10); + + if (loading) { + return ; + } + + return ( + <> + {workloadItem && ( + + )} + + ); +}; diff --git a/plugins/kiali/src/components/VirtualList/Renderers.tsx b/plugins/kiali/src/components/VirtualList/Renderers.tsx index 92c925fbfb..40839e3060 100644 --- a/plugins/kiali/src/components/VirtualList/Renderers.tsx +++ b/plugins/kiali/src/components/VirtualList/Renderers.tsx @@ -2,7 +2,16 @@ import * as React from 'react'; import { Link } from '@backstage/core-components'; -import { Chip, TableCell, Tooltip } from '@material-ui/core'; +import { + Button, + Chip, + Drawer, + IconButton, + TableCell, + Tooltip, +} from '@material-ui/core'; +// eslint-disable-next-line no-restricted-imports +import { Close } from '@material-ui/icons'; import { KialiIcon, serverConfig } from '../../config'; import { isWaypoint } from '../../helpers/LabelFilterHelper'; @@ -15,10 +24,13 @@ import { ValidationStatus } from '../../types/IstioObjects'; import { ComponentStatus } from '../../types/IstioStatus'; import { NamespaceInfo } from '../../types/NamespaceInfo'; import { ServiceListItem } from '../../types/ServiceList'; -import { ENTITY } from '../../types/types'; +import { DRAWER, ENTITY } from '../../types/types'; import { WorkloadListItem } from '../../types/Workload'; import { getReconciliationCondition } from '../../utils/IstioConfigUtils'; import { JanusObjectLink } from '../../utils/janusLinks'; +import { AppDetailsDrawer } from '../Drawers/AppDetailsDrawer'; +import { ServiceDetailsDrawer } from '../Drawers/ServiceDetailsDrawer'; +import { WorkloadDetailsDrawer } from '../Drawers/WorkloadDetailsDrawer'; import { StatefulFilters } from '../Filters/StatefulFilters'; import { HealthIndicator } from '../Health/HealthIndicator'; import { NamespaceMTLSStatus } from '../MTls/NamespaceMTLSStatus'; @@ -52,6 +64,69 @@ export const actionRenderer = ( ); }; +const DrawerDiv = ({ + name, + namespace, + config, +}: { + name: string; + namespace: string; + config: string; +}) => { + const [isOpen, toggleDrawer] = React.useState(false); + + const DrawerContent = ({ + toggleDrawer2, + }: { + toggleDrawer2: (isOpen: boolean) => void; + }) => { + return ( +
    +
    + toggleDrawer2(false)} + color="inherit" + style={{ right: '0', position: 'absolute', top: '5px' }} + > + + +
    +
    +
    + {config === 'workloads' && ( + + )} + {config === 'services' && ( + + )} + {config === 'applications' && ( + + )} +
    +
    + ); + }; + + return ( + <> + + toggleDrawer(false)}> + + + + ); +}; + export const item: Renderer = ( resource: TResource, config: Resource, @@ -78,13 +153,26 @@ export const item: Renderer = ( } } + if (view === DRAWER) { + return ( + + + + ); + } return ( - {view !== ENTITY && ( + {view !== ENTITY && view !== DRAWER && ( )} = ( key={`VirtuaItem_Namespace_${resource.namespace}_${item.name}`} style={{ verticalAlign: 'middle', whiteSpace: 'nowrap' }} > - {view !== ENTITY && ( + {view !== ENTITY && view !== DRAWER && ( )} {resource.namespace} @@ -172,12 +260,12 @@ export const labels: Renderer = ( }${resource.name}`} style={{ verticalAlign: 'middle', paddingBottom: '0.25rem' }} > - {view === ENTITY && resource.labels && ( + {(view === ENTITY || view === DRAWER) && resource.labels && ( )} - {view !== ENTITY && labelsView} + {view !== ENTITY && view !== DRAWER && labelsView} ); }; diff --git a/plugins/kiali/src/components/VirtualList/VirtualList.tsx b/plugins/kiali/src/components/VirtualList/VirtualList.tsx index a1a364e027..c26e3f3093 100644 --- a/plugins/kiali/src/components/VirtualList/VirtualList.tsx +++ b/plugins/kiali/src/components/VirtualList/VirtualList.tsx @@ -18,7 +18,7 @@ import { kialiStyle } from '../../styles/StyleUtils'; import { Namespace } from '../../types/Namespace'; import { NamespaceInfo } from '../../types/NamespaceInfo'; import { SortField } from '../../types/SortFilters'; -import { ENTITY } from '../../types/types'; +import { DRAWER, ENTITY } from '../../types/types'; import { StatefulFiltersProps } from '../Filters/StatefulFilters'; import { config, RenderResource, Resource, ResourceType } from './Config'; import { VirtualItem } from './VirtualItem'; @@ -156,7 +156,7 @@ export const VirtualList = ( key={`column_${index}`} align="center" style={ - listProps.view === ENTITY + listProps.view === ENTITY || listProps.view === DRAWER ? tableEntityHeaderStyle : tableHeaderStyle } @@ -176,8 +176,9 @@ export const VirtualList = ( handleRequestSort(e, column.title.toLowerCase()) } > - {listProps.view === ENTITY && - column.title === 'Configuration' + {listProps.view === ENTITY || + (listProps.view === DRAWER && + column.title === 'Configuration') ? 'CONFIG' : column.title.toUpperCase()} diff --git a/plugins/kiali/src/dynamic/EntityKialiResourcesCard.tsx b/plugins/kiali/src/dynamic/EntityKialiResourcesCard.tsx new file mode 100644 index 0000000000..b1932d25ad --- /dev/null +++ b/plugins/kiali/src/dynamic/EntityKialiResourcesCard.tsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +import { useRef } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { + CardTab, + CodeSnippet, + EmptyState, + TabbedCard, +} from '@backstage/core-components'; +import { useEntity } from '@backstage/plugin-catalog-react'; + +import { Box } from '@material-ui/core'; + +import { AppListPage } from '../pages/AppList/AppListPage'; +import { ServiceListPage } from '../pages/ServiceList/ServiceListPage'; +import { WorkloadListPage } from '../pages/WorkloadList/WorkloadListPage'; +import { DRAWER } from '../types/types'; + +const tabStyle: React.CSSProperties = { + overflowY: 'scroll', + maxHeight: '400px', +}; + +const tabs = ['workload', 'service', 'application']; + +export const EntityKialiResourcesCard = () => { + const { entity } = useEntity(); + const location = useLocation(); + const [element, setElement] = React.useState(); + const prevElement = useRef(element); + const [renderCount, setRenderCount] = React.useState(0); + + const getInitValue = (): string => { + const hash = location.hash.replace(/^#,?\s*/, ''); + const data = hash.split('/'); + + if (data.length > 1 && data[1] !== element) { + setElement(data[1]); + } + if (tabs.includes(data[0])) { + return data[0]; + } + return tabs[0]; + }; + const [value, setValue] = React.useState(getInitValue()); + + const navigate = useNavigate(); + + const handleChange = ( + _: React.ChangeEvent<{}>, + newValue: string | number, + ) => { + setValue(newValue); + navigate(`#${newValue}`); + }; + + React.useEffect(() => { + // This time out is needed to have rendered the context and be able to call the element to open the drawer + const timeout = setTimeout(() => { + setRenderCount(prevCount => prevCount + 1); + }, 1000); + return () => clearTimeout(timeout); + }, []); + + React.useEffect(() => { + const hash = location.hash.replace(/^#,?\s*/, ''); + const data = hash.split('/'); + if (data.length > 0) { + const val = data[0]; + if (val !== value) { + setValue(val); + setTimeout(() => { + setRenderCount(prevCount => prevCount + 1); + }, 1000); + } + } + }, [location.hash, value]); + + React.useEffect(() => { + if (element && element !== prevElement.current && renderCount > 0) { + setTimeout(() => { + const drawer = document.getElementById(`drawer_${element}`); + if (drawer) { + drawer.click(); + } + prevElement.current = element; + }, 1000); + } + }, [element, renderCount]); + + return !entity ? ( + + Kiali detected the annotations +
    + This is the entity loaded. + + + +
    + + } + /> + ) : ( + + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    +
    + ); +}; diff --git a/plugins/kiali/src/dynamic/KialiContext.tsx b/plugins/kiali/src/dynamic/KialiContext.tsx new file mode 100644 index 0000000000..552cd93b2d --- /dev/null +++ b/plugins/kiali/src/dynamic/KialiContext.tsx @@ -0,0 +1,77 @@ +import React, { createContext, useContext, useMemo } from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import useDebounce from 'react-use/lib/useDebounce'; + +import { useApi } from '@backstage/core-plugin-api'; +import { useEntity } from '@backstage/plugin-catalog-react'; + +import { KUBERNETES_NAMESPACE } from '../components/Router'; +import { NamespaceInfo } from '../pages/Overview/NamespaceInfo'; +import { getNamespaces } from '../pages/Overview/OverviewPage'; +import { kialiApiRef } from '../services/Api'; + +type KialiEntityContextType = { + data: NamespaceInfo[] | null; + loading: boolean; + error: Error | null; +}; + +const KialiEntityContext = createContext( + {} as KialiEntityContextType, +); + +export const KialiContextProvider = (props: any) => { + const { entity } = useEntity(); + const kialiClient = useApi(kialiApiRef); + + const [{ value: namespace, loading, error: asyncError }, refresh] = + useAsyncFn( + async () => { + const annotations = entity?.metadata?.annotations || undefined; + let ns: string[]; + if (!annotations) { + ns = []; + } else { + const ant = decodeURIComponent(annotations[KUBERNETES_NAMESPACE]); + if (ant) { + ns = ant.split(','); + } + ns = []; + } + const filteredNs = await kialiClient + .getNamespaces() + .then(namespacesResponse => { + const allNamespaces: NamespaceInfo[] = getNamespaces( + namespacesResponse, + [], + ); + const namespaceInfos = allNamespaces.filter(nsInfo => + ns.includes(nsInfo.name), + ); + return namespaceInfos; + }); + return filteredNs; + }, + [], + { loading: true }, + ); + useDebounce(refresh, 10); + const isError = Boolean(asyncError); + const error = isError ? asyncError || Object.assign(new Error()) : null; + + const value = useMemo( + () => ({ + data: isError || loading ? null : (namespace as NamespaceInfo[]), + loading, + error, + }), + [namespace, isError, loading, error], + ); + + return ( + + {props.children} + + ); +}; +export const useKialiEntityContext = () => useContext(KialiEntityContext); diff --git a/plugins/kiali/src/helpers/namespaces.ts b/plugins/kiali/src/helpers/namespaces.ts index ad7bab6e9e..f5a3a54b41 100644 --- a/plugins/kiali/src/helpers/namespaces.ts +++ b/plugins/kiali/src/helpers/namespaces.ts @@ -1,6 +1,22 @@ +import { Entity } from '@backstage/catalog-model'; + +import { KUBERNETES_NAMESPACE } from '../components/Router'; + export const nsEqual = (ns: string[], ns2: string[]): boolean => { return ( ns.length === ns2.length && ns.every((value: any, index: number) => value === ns2[index]) ); }; + +export const getEntityNs = (entity: Entity): string[] => { + const annotations = entity?.metadata?.annotations || undefined; + if (!annotations) { + return []; + } + const ant = decodeURIComponent(annotations[KUBERNETES_NAMESPACE]); + if (ant) { + return ant.split(','); + } + return []; +}; diff --git a/plugins/kiali/src/index.ts b/plugins/kiali/src/index.ts index 524964a7a3..d05a6417cc 100644 --- a/plugins/kiali/src/index.ts +++ b/plugins/kiali/src/index.ts @@ -1,3 +1,8 @@ -export { kialiPlugin, EntityKialiContent, KialiPage } from './plugin'; +export { + kialiPlugin, + EntityKialiContent, + KialiPage, + EntityKialiResourcesCard, +} from './plugin'; export { default as KialiIcon } from '@mui/icons-material/Troubleshoot'; diff --git a/plugins/kiali/src/pages/AppDetails/AppDescription.tsx b/plugins/kiali/src/pages/AppDetails/AppDescription.tsx index 12c7032b92..d9d23267a5 100644 --- a/plugins/kiali/src/pages/AppDetails/AppDescription.tsx +++ b/plugins/kiali/src/pages/AppDetails/AppDescription.tsx @@ -14,6 +14,7 @@ import * as H from '../../types/Health'; type AppDescriptionProps = { app?: App; health?: H.Health; + view?: string; }; const iconStyle = kialiStyle({ @@ -36,25 +37,29 @@ export const AppDescription: React.FC = ( return props.app ? ( - - -
    - -
    + + +
    + +
    - {props.app.name} + {props.app.name} - - - -
    + + + +
    - {props.app.cluster && isMultiCluster && ( -
    - {props.app.cluster} -
    - )} -
    + {props.app.cluster && isMultiCluster && ( +
    + {props.app.cluster} +
    + )} + + } + /> = ( services={props.app ? props.app.serviceNames : []} health={props.health} cluster={props.app?.cluster} + view={props.view} />
    diff --git a/plugins/kiali/src/pages/AppDetails/AppInfo.tsx b/plugins/kiali/src/pages/AppDetails/AppInfo.tsx index dea05e12d0..d208fff220 100644 --- a/plugins/kiali/src/pages/AppDetails/AppInfo.tsx +++ b/plugins/kiali/src/pages/AppDetails/AppInfo.tsx @@ -5,19 +5,26 @@ import { Grid } from '@material-ui/core'; import { App } from '../../types/App'; import { DurationInSeconds } from '../../types/Common'; import { AppHealth } from '../../types/Health'; +import { DRAWER, ENTITY } from '../../types/types'; import { AppDescription } from './AppDescription'; type AppInfoProps = { app?: App; duration: DurationInSeconds; health?: AppHealth; + view?: string; }; export const AppInfo = (appProps: AppInfoProps) => { + const size = appProps.view === ENTITY || appProps.view === DRAWER ? 12 : 4; return ( - - + + ); diff --git a/plugins/kiali/src/pages/AppList/AppListPage.tsx b/plugins/kiali/src/pages/AppList/AppListPage.tsx index a9e4a21360..16c5a85a92 100644 --- a/plugins/kiali/src/pages/AppList/AppListPage.tsx +++ b/plugins/kiali/src/pages/AppList/AppListPage.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { useRef } from 'react'; import { useAsyncFn, useDebounce } from 'react-use'; +import { Entity } from '@backstage/catalog-model'; import { Content } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; @@ -10,7 +11,7 @@ import * as FilterHelper from '../../components/FilterList/FilterHelper'; import { TimeDurationComponent } from '../../components/Time/TimeDurationComponent'; import { VirtualList } from '../../components/VirtualList/VirtualList'; import { isMultiCluster } from '../../config'; -import { nsEqual } from '../../helpers/namespaces'; +import { getEntityNs, nsEqual } from '../../helpers/namespaces'; import { getErrorString, kialiApiRef } from '../../services/Api'; import { KialiAppState, KialiContext } from '../../store'; import { baseStyle } from '../../styles/StyleUtils'; @@ -20,7 +21,10 @@ import { NamespaceInfo } from '../Overview/NamespaceInfo'; import { getNamespaces } from '../Overview/OverviewPage'; import * as AppListClass from './AppListClass'; -export const AppListPage = (props: { view?: string }): React.JSX.Element => { +export const AppListPage = (props: { + view?: string; + entity?: Entity; +}): React.JSX.Element => { const kialiClient = useApi(kialiApiRef); const [namespaces, setNamespaces] = React.useState([]); const [allApps, setApps] = React.useState([]); @@ -28,7 +32,9 @@ export const AppListPage = (props: { view?: string }): React.JSX.Element => { FilterHelper.currentDuration(), ); const kialiState = React.useContext(KialiContext) as KialiAppState; - const activeNs = kialiState.namespaces.activeNamespaces.map(ns => ns.name); + const activeNs = props.entity + ? getEntityNs(props.entity) + : kialiState.namespaces.activeNamespaces.map(ns => ns.name); const prevActiveNs = useRef(activeNs); const prevDuration = useRef(duration); const [loadingD, setLoading] = React.useState(true); @@ -78,10 +84,11 @@ export const AppListPage = (props: { view?: string }): React.JSX.Element => { }); setApps(appListItems); }) - .catch(err => - kialiState.alertUtils!.add( - `Could not fetch services: ${getErrorString(err)}`, - ), + .catch( + err => + kialiState.alertUtils?.add( + `Could not fetch services: ${getErrorString(err)}`, + ), ); }; diff --git a/plugins/kiali/src/pages/ServiceDetails/ServiceDescription.tsx b/plugins/kiali/src/pages/ServiceDetails/ServiceDescription.tsx index 2d80e03b00..60525756bd 100644 --- a/plugins/kiali/src/pages/ServiceDetails/ServiceDescription.tsx +++ b/plugins/kiali/src/pages/ServiceDetails/ServiceDescription.tsx @@ -24,6 +24,7 @@ import { ServiceDetailsInfo, WorkloadOverview } from '../../types/ServiceInfo'; interface ServiceInfoDescriptionProps { namespace: string; serviceDetails?: ServiceDetailsInfo; + view?: string; } const resourceListStyle = kialiStyle({ @@ -235,6 +236,7 @@ export const ServiceDescription: React.FC = ( workloads={workloads} health={props.serviceDetails?.health} cluster={props.serviceDetails?.cluster} + view={props.view} /> diff --git a/plugins/kiali/src/pages/ServiceDetails/ServiceInfo.tsx b/plugins/kiali/src/pages/ServiceDetails/ServiceInfo.tsx index 7315bf091f..d9e4e172c0 100644 --- a/plugins/kiali/src/pages/ServiceDetails/ServiceInfo.tsx +++ b/plugins/kiali/src/pages/ServiceDetails/ServiceInfo.tsx @@ -22,6 +22,7 @@ import { } from '../../types/IstioObjects'; import { ServiceId } from '../../types/ServiceId'; import { ServiceDetailsInfo } from '../../types/ServiceInfo'; +import { DRAWER, ENTITY } from '../../types/types'; import { ServiceDescription } from './ServiceDescription'; import { ServiceNetwork } from './ServiceNetwork'; @@ -34,6 +35,7 @@ interface Props extends ServiceId { peerAuthentications: PeerAuthentication[]; validations: Validations; istioAPIEnabled: boolean; + view?: string; } export const ServiceInfo = (serviceProps: Props) => { @@ -110,29 +112,36 @@ export const ServiceInfo = (serviceProps: Props) => { ), ); + const size = + serviceProps.view === ENTITY || serviceProps.view === DRAWER ? 12 : 4; return ( <> {serviceProps.serviceDetails && ( - + - - - - - - + {serviceProps.view !== DRAWER && ( + <> + + + + + + + + )} )} diff --git a/plugins/kiali/src/pages/ServiceList/ServiceListPage.tsx b/plugins/kiali/src/pages/ServiceList/ServiceListPage.tsx index 3e375f76b4..9d1b36893e 100644 --- a/plugins/kiali/src/pages/ServiceList/ServiceListPage.tsx +++ b/plugins/kiali/src/pages/ServiceList/ServiceListPage.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { useRef } from 'react'; import { useAsyncFn, useDebounce } from 'react-use'; +import { Entity } from '@backstage/catalog-model'; import { Content } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; @@ -13,7 +14,7 @@ import { Toggles } from '../../components/Filters/StatefulFilters'; import { TimeDurationComponent } from '../../components/Time/TimeDurationComponent'; import { VirtualList } from '../../components/VirtualList/VirtualList'; import { isMultiCluster } from '../../config'; -import { nsEqual } from '../../helpers/namespaces'; +import { getEntityNs, nsEqual } from '../../helpers/namespaces'; import { getErrorString, kialiApiRef } from '../../services/Api'; import { KialiAppState, KialiContext } from '../../store'; import { baseStyle } from '../../styles/StyleUtils'; @@ -29,6 +30,7 @@ import { getNamespaces } from '../Overview/OverviewPage'; export const ServiceListPage = (props: { view?: string; + entity?: Entity; }): React.JSX.Element => { const kialiClient = useApi(kialiApiRef); const [namespaces, setNamespaces] = React.useState([]); @@ -37,7 +39,9 @@ export const ServiceListPage = (props: { FilterHelper.currentDuration(), ); const kialiState = React.useContext(KialiContext) as KialiAppState; - const activeNs = kialiState.namespaces.activeNamespaces.map(ns => ns.name); + const activeNs = props.entity + ? getEntityNs(props.entity) + : kialiState.namespaces.activeNamespaces.map(ns => ns.name); const prevActiveNs = useRef(activeNs); const prevDuration = useRef(duration); const activeToggles: ActiveTogglesInfo = Toggles.getToggles(); @@ -146,10 +150,11 @@ export const ServiceListPage = (props: { }); setServices(serviceListItems); }) - .catch(err => - kialiState.alertUtils!.add( - `Could not fetch services: ${getErrorString(err)}`, - ), + .catch( + err => + kialiState.alertUtils?.add( + `Could not fetch services: ${getErrorString(err)}`, + ), ); }; diff --git a/plugins/kiali/src/pages/WorkloadDetails/WorkloadInfo.tsx b/plugins/kiali/src/pages/WorkloadDetails/WorkloadInfo.tsx index 4dcca129dd..074fa64877 100644 --- a/plugins/kiali/src/pages/WorkloadDetails/WorkloadInfo.tsx +++ b/plugins/kiali/src/pages/WorkloadDetails/WorkloadInfo.tsx @@ -22,6 +22,7 @@ import { Validations, ValidationTypes, } from '../../types/IstioObjects'; +import { DRAWER, ENTITY } from '../../types/types'; import { Workload } from '../../types/Workload'; import { WorkloadPods } from './WorkloadPods'; import { WorkloadDescription } from './WorkloadsDescription'; @@ -32,6 +33,7 @@ type WorkloadInfoProps = { namespace?: string; workload: Workload; health?: WorkloadHealth; + view?: string; }; export const WorkloadInfo = (workloadProps: WorkloadInfoProps) => { @@ -297,36 +299,45 @@ export const WorkloadInfo = (workloadProps: WorkloadInfoProps) => { return validations; }; + const size = + workloadProps.view === ENTITY || workloadProps.view === DRAWER ? 12 : 4; return ( <> {workloadProps.workload && ( - + - - - - - - + {workloadProps.view !== DRAWER && ( + <> + + + + + + + + )} )} diff --git a/plugins/kiali/src/pages/WorkloadDetails/WorkloadsDescription.tsx b/plugins/kiali/src/pages/WorkloadDetails/WorkloadsDescription.tsx index 6ee775c9e4..47d298954a 100644 --- a/plugins/kiali/src/pages/WorkloadDetails/WorkloadsDescription.tsx +++ b/plugins/kiali/src/pages/WorkloadDetails/WorkloadsDescription.tsx @@ -33,6 +33,7 @@ type WorkloadDescriptionProps = { entity?: boolean; namespace: string; workload: Workload; + view?: string; }; const resourceListStyle = kialiStyle({ @@ -46,17 +47,17 @@ const resourceListStyle = kialiStyle({ }, }); -const iconStyle = kialiStyle({ +export const iconStyle = kialiStyle({ display: 'inline-block', }); -const infoStyle = kialiStyle({ +export const infoStyle = kialiStyle({ marginLeft: '0.5rem', verticalAlign: '-0.125rem', display: 'inline-block', }); -const healthIconStyle = kialiStyle({ +export const healthIconStyle = kialiStyle({ marginLeft: '0.5rem', verticalAlign: '-0.075rem', }); @@ -269,6 +270,7 @@ export const WorkloadDescription: React.FC = ( services={services} health={props.health} cluster={props.workload.cluster} + view={props.view} waypointWorkloads={ isWaypoint(props.workload.labels) ? props.workload.waypointWorkloads diff --git a/plugins/kiali/src/pages/WorkloadList/WorkloadListPage.tsx b/plugins/kiali/src/pages/WorkloadList/WorkloadListPage.tsx index 1b66a0e4f6..6c7cba0a8f 100644 --- a/plugins/kiali/src/pages/WorkloadList/WorkloadListPage.tsx +++ b/plugins/kiali/src/pages/WorkloadList/WorkloadListPage.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { useRef } from 'react'; import { useAsyncFn, useDebounce } from 'react-use'; +import { Entity } from '@backstage/catalog-model'; import { Content } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; @@ -12,7 +13,8 @@ import * as FilterHelper from '../../components/FilterList/FilterHelper'; import { TimeDurationComponent } from '../../components/Time/TimeDurationComponent'; import { VirtualList } from '../../components/VirtualList/VirtualList'; import { isMultiCluster } from '../../config'; -import { nsEqual } from '../../helpers/namespaces'; +import { useKialiEntityContext } from '../../dynamic/KialiContext'; +import { getEntityNs, nsEqual } from '../../helpers/namespaces'; import { getErrorString, kialiApiRef } from '../../services/Api'; import { KialiAppState, KialiContext } from '../../store'; import { baseStyle } from '../../styles/StyleUtils'; @@ -21,7 +23,7 @@ import { WorkloadListItem } from '../../types/Workload'; import { NamespaceInfo } from '../Overview/NamespaceInfo'; import { getNamespaces } from '../Overview/OverviewPage'; -export const WorkloadListPage = (props: { view?: string }) => { +export const WorkloadListPage = (props: { view?: string; entity?: Entity }) => { const kialiClient = useApi(kialiApiRef); const [namespaces, setNamespaces] = React.useState([]); const [allWorkloads, setWorkloads] = React.useState([]); @@ -29,7 +31,11 @@ export const WorkloadListPage = (props: { view?: string }) => { FilterHelper.currentDuration(), ); const kialiState = React.useContext(KialiContext) as KialiAppState; - const activeNs = kialiState.namespaces.activeNamespaces.map(ns => ns.name); + const kialiContext = useKialiEntityContext(); + + const activeNs = props.entity + ? getEntityNs(props.entity) + : kialiState.namespaces.activeNamespaces.map(ns => ns.name); const prevActiveNs = useRef(activeNs); const prevDuration = useRef(duration); const [loadingData, setLoadingData] = React.useState(true); @@ -54,23 +60,29 @@ export const WorkloadListPage = (props: { view?: string }) => { }); setWorkloads(wkList); }) - .catch(err => - kialiState.alertUtils!.add( + .catch(err => { + kialiState.alertUtils?.add( `Could not fetch workloads: ${getErrorString(err)}`, - ), - ); + ); + }); }; const load = async () => { - kialiClient.getNamespaces().then(namespacesResponse => { - const allNamespaces: NamespaceInfo[] = getNamespaces( - namespacesResponse, - namespaces, - ); - const nsl = allNamespaces.filter(ns => activeNs.includes(ns.name)); - setNamespaces(nsl); - fetchWorkloads(nsl, duration); - }); + if (kialiContext.data) { + setNamespaces(kialiContext.data); + fetchWorkloads(kialiContext.data, duration); + } else { + kialiClient.getNamespaces().then(namespacesResponse => { + const allNamespaces: NamespaceInfo[] = getNamespaces( + namespacesResponse, + namespaces, + ); + const nsl = allNamespaces.filter(ns => activeNs.includes(ns.name)); + setNamespaces(nsl); + fetchWorkloads(nsl, duration); + }); + } + // Add a delay so it doesn't look like a flash setTimeout(() => { setLoadingData(false); @@ -140,6 +152,7 @@ export const WorkloadListPage = (props: { view?: string }) => { hiddenColumns={hiddenColumns} view={props.view} loading={loadingData} + data-test="virtual-list" />
    diff --git a/plugins/kiali/src/plugin.ts b/plugins/kiali/src/plugin.ts index e99df4fad4..aa7fd9e49f 100644 --- a/plugins/kiali/src/plugin.ts +++ b/plugins/kiali/src/plugin.ts @@ -1,5 +1,6 @@ import { createApiFactory, + createComponentExtension, createPlugin, createRoutableExtension, discoveryApiRef, @@ -40,6 +41,17 @@ export const KialiPage = kialiPlugin.provide( }), ); +export const EntityKialiResourcesCard = kialiPlugin.provide( + createComponentExtension({ + name: 'EntityKialiResourcesCard', + component: { + lazy: () => + import('./dynamic/EntityKialiResourcesCard').then( + m => m.EntityKialiResourcesCard, + ), + }, + }), +); /** * Props of EntityExampleComponent * diff --git a/plugins/kiali/src/types/ErrorRate/ErrorRate.ts b/plugins/kiali/src/types/ErrorRate/ErrorRate.ts index 99b8549657..d2416369cd 100644 --- a/plugins/kiali/src/types/ErrorRate/ErrorRate.ts +++ b/plugins/kiali/src/types/ErrorRate/ErrorRate.ts @@ -97,11 +97,11 @@ const getAggregate = ( outbound: RequestTolerance[]; } => { // Get all tolerances where direction is inbound - const inboundTolerances = conf.filter(tol => + const inboundTolerances = conf?.filter(tol => checkExpr(tol.direction, 'inbound'), ); // Get all tolerances where direction is outbound - const outboundTolerances = conf.filter(tol => + const outboundTolerances = conf?.filter(tol => checkExpr(tol.direction, 'outbound'), ); @@ -164,7 +164,7 @@ export const calculateErrorRate = ( const rateAnnotation = new RateHealth(requests.healthAnnotations); const conf = rateAnnotation.toleranceConfig || - getRateHealthConfig(ns, name, kind).tolerance; + getRateHealthConfig(ns, name, kind)?.tolerance; // Get aggregate const status = getAggregate(requests, conf); diff --git a/plugins/kiali/src/types/Health.ts b/plugins/kiali/src/types/Health.ts index 0f51f8559d..61ee51297e 100644 --- a/plugins/kiali/src/types/Health.ts +++ b/plugins/kiali/src/types/Health.ts @@ -314,15 +314,17 @@ export abstract class Health { // Check if the config applied is the kiali defaults one const tolConfDefault = serverConfig.healthConfig.rate[serverConfig.healthConfig.rate.length - 1] - .tolerance; - for (const tol of tolConfDefault) { - // Check if the tolerance applied is one of kiali defaults - if ( - this.health.statusConfig && - tol === this.health.statusConfig.threshold - ) { - // In the case is a kiali's default return undefined - return undefined; + ?.tolerance; + if (tolConfDefault) { + for (const tol of tolConfDefault) { + // Check if the tolerance applied is one of kiali defaults + if ( + this.health.statusConfig && + tol === this.health.statusConfig.threshold + ) { + // In the case is a kiali's default return undefined + return undefined; + } } } // Otherwise return the threshold configuration that kiali used to calculate the status diff --git a/plugins/kiali/src/types/types.ts b/plugins/kiali/src/types/types.ts index a65a821891..d7af094bdc 100644 --- a/plugins/kiali/src/types/types.ts +++ b/plugins/kiali/src/types/types.ts @@ -1,6 +1,7 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; export const ENTITY = 'entity'; +export const DRAWER = 'drawer'; export interface KialiError { detail: string; diff --git a/plugins/kiali/tests/entityResources.spec.ts b/plugins/kiali/tests/entityResources.spec.ts new file mode 100644 index 0000000000..6ecc8af2aa --- /dev/null +++ b/plugins/kiali/tests/entityResources.spec.ts @@ -0,0 +1,37 @@ +import { expect, Page, test } from '@playwright/test'; + +test.describe('Entity resources', () => { + let page: Page; + + test.describe('kiali resources', () => { + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext(); + page = await context.newPage(); + await page.goto('/kiali-entity-card'); + page.locator('[data-test="kiali-tabbed-card"]'); + }); + + test.afterAll(async ({ browser }) => { + await browser.close(); + }); + + test('Workloads content', async () => { + expect(page.locator('[data-test="virtual-list"]')).toBeDefined(); + }); + + test('Workloads Drawer', async () => { + await page.locator('#drawer_bookinfo_details-v1').click(); + expect(page.locator('[data-test="drawer"]')).toBeDefined(); + }); + + test('Close drawer', async () => { + await page.locator('#close_drawer').click(); + expect(page.locator('[data-test="service-tab"]')).toBeDefined(); + }); + + test('Services tab', async () => { + await page.locator('[data-test="service-tab"]').click(); + expect(page.locator('#drawer_bookinfo_details')).toBeDefined(); + }); + }); +});