diff --git a/plugin/src/kiali/README.md b/plugin/src/kiali/README.md index 4343d501..574d5791 100644 --- a/plugin/src/kiali/README.md +++ b/plugin/src/kiali/README.md @@ -2,6 +2,6 @@ Copy of Kiali frontend source code Kiali frontend source originated from: -* git ref: v1.87.0 -* git commit: 1913b4cfd5d0f1d4b2261b990fead586c6eed876 -* GitHub URL: https://github.com/kiali/kiali/tree/1913b4cfd5d0f1d4b2261b990fead586c6eed876/frontend/src +* git ref: v1.88 +* git commit: 139b6e879b2fc36f807fcd0d94d43b3fc7dc64e4 +* GitHub URL: https://github.com/kiali/kiali/tree/139b6e879b2fc36f807fcd0d94d43b3fc7dc64e4/frontend/src diff --git a/plugin/src/kiali/actions/__tests__/LoginAction.test.ts b/plugin/src/kiali/actions/__tests__/LoginAction.test.ts index 08a2819f..9012e907 100644 --- a/plugin/src/kiali/actions/__tests__/LoginAction.test.ts +++ b/plugin/src/kiali/actions/__tests__/LoginAction.test.ts @@ -3,7 +3,7 @@ import { LoginActions } from '../LoginActions'; import { LoginStatus } from '../../store/Store'; const session = { - expiresOn: '018-05-29 21:51:40.186179601 +0200 CEST m=+36039.431579761', + expiresOn: '2018-05-29 21:51:40.186179601 +0200 CEST m=+36039.431579761', username: 'admin' }; diff --git a/plugin/src/kiali/app/App.tsx b/plugin/src/kiali/app/App.tsx index 84f4aebe..05b7026f 100644 --- a/plugin/src/kiali/app/App.tsx +++ b/plugin/src/kiali/app/App.tsx @@ -2,13 +2,11 @@ import axios from 'axios'; import * as React from 'react'; import { PersistGate } from 'redux-persist/lib/integration/react'; import { Provider } from 'react-redux'; -import { Router, withRouter } from 'react-router-dom'; import * as Visibility from 'visibilityjs'; import { GlobalActions } from '../actions/GlobalActions'; import { Navigation } from '../components/Nav/Navigation'; import { persistor, store } from '../store/ConfigStore'; import { AuthenticationController } from './AuthenticationController'; -import { history } from './History'; import { InitializingScreen } from './InitializingScreen'; import { StartupInitializer } from './StartupInitializer'; import { LoginPage } from '../pages/Login/LoginPage'; @@ -30,12 +28,12 @@ if (Visibility.hidden()) { store.dispatch(GlobalActions.setPageVisibilityVisible()); } -const getIsLoadingState = () => { +const getIsLoadingState = (): boolean => { const state = store.getState(); return state && state.globalState.loadingCounter > 0; }; -const decrementLoadingCounter = () => { +const decrementLoadingCounter = (): void => { if (getIsLoadingState()) { store.dispatch(GlobalActions.decrementLoadingCounter()); } @@ -83,49 +81,25 @@ axios.interceptors.response.use( } ); -type AppState = { - isInitialized: boolean; +export const App: React.FC = () => { + const [isInitialized, setIsInitialized] = React.useState(false); + + return ( + }> + + } persistor={persistor}> + {isInitialized ? ( + ( + + )} + protectedAreaComponent={} + /> + ) : ( + setIsInitialized(true)} /> + )} + + + + ); }; - -export class App extends React.Component<{}, AppState> { - private protectedArea: React.ReactNode; - - constructor(props: {}) { - super(props); - this.state = { - isInitialized: false - }; - - const Navigator = withRouter(Navigation); - this.protectedArea = ( - - - - ); - } - - render() { - return ( - }> - - } persistor={persistor}> - {this.state.isInitialized ? ( - ( - - )} - protectedAreaComponent={this.protectedArea} - /> - ) : ( - - )} - - - - ); - } - - private initializationFinishedHandler = () => { - this.setState({ isInitialized: true }); - }; -} diff --git a/plugin/src/kiali/app/AuthenticationController.tsx b/plugin/src/kiali/app/AuthenticationController.tsx index 000685fa..1577d7e4 100644 --- a/plugin/src/kiali/app/AuthenticationController.tsx +++ b/plugin/src/kiali/app/AuthenticationController.tsx @@ -17,7 +17,7 @@ import { setServerConfig, serverConfig, humanDurations } from '../config/ServerC import { AuthStrategy } from '../types/Auth'; import { TracingInfo } from '../types/TracingInfo'; import { LoginActions } from '../actions/LoginActions'; -import { history } from './History'; +import { location, router } from './History'; import { NamespaceActions } from 'actions/NamespaceAction'; import { Namespace } from 'types/Namespace'; import { UserSettingsActions } from 'actions/UserSettingsActions'; @@ -114,7 +114,7 @@ class AuthenticationControllerComponent extends React.Component< stage: LoginStage.LOGGED_IN_AT_LOAD }); } else { - this.props.setLandingRoute(history.location.pathname + history.location.search); + this.props.setLandingRoute(location.getPathname() + location.getSearch()); } } @@ -203,7 +203,7 @@ class AuthenticationControllerComponent extends React.Component< this.applyUIDefaults(); if (this.props.landingRoute) { - history.replace(this.props.landingRoute); + router.navigate(this.props.landingRoute, { replace: true }); this.props.setLandingRoute(undefined); } diff --git a/plugin/src/kiali/app/History.ts b/plugin/src/kiali/app/History.tsx similarity index 67% rename from plugin/src/kiali/app/History.ts rename to plugin/src/kiali/app/History.tsx index c003bd52..a5edb815 100644 --- a/plugin/src/kiali/app/History.ts +++ b/plugin/src/kiali/app/History.tsx @@ -1,31 +1,45 @@ -import { createBrowserHistory, createMemoryHistory, createHashHistory } from 'history'; +import * as React from 'react'; import { toValidDuration } from '../config/ServerConfig'; import { BoundsInMilliseconds } from 'types/Common'; +import { RouteObject, createBrowserRouter, createHashRouter, createMemoryRouter } from 'react-router-dom-v5-compat'; + +export const createRouter = (routes: RouteObject[], basename?: string): any => { + const baseName = basename ?? rootBasename; -const createHistory = (baseName: string): any => { return process.env.TEST_RUNNER - ? createMemoryHistory() + ? createMemoryRouter(routes, { basename: baseName }) : historyMode === 'hash' - ? createHashHistory() - : createBrowserHistory({ basename: baseName }); + ? createHashRouter(routes, { basename: baseName }) + : createBrowserRouter(routes, { basename: baseName }); }; +export const webRoot = (window as any).WEB_ROOT ?? '/'; +export const rootBasename = webRoot !== '/' ? `${webRoot}/console` : '/console'; +const historyMode = (window as any).HISTORY_MODE ?? 'browser'; + /** * Some platforms set a different basename for each page (e.g., Openshift Console) * A setHistory method is defined to be able to modify the history basename when user * routes to a different page within Kiali in these platforms. * This method is not used in standalone Kiali application */ -export const setHistory = (baseName: string): void => { - history = createHistory(baseName); +export const setRouter = (routes: RouteObject[], basename?: string): void => { + router = createRouter(routes, basename); }; -const webRoot = (window as any).WEB_ROOT ? (window as any).WEB_ROOT : undefined; -const baseName = webRoot && webRoot !== '/' ? `${webRoot}/console` : '/console'; -const historyMode = (window as any).HISTORY_MODE ? (window as any).HISTORY_MODE : 'browser'; -let history = createHistory(baseName); +let router = createRouter([{ element: <> }], rootBasename); + +const location = { + getPathname: (): string => { + return router.state.location.pathname.replace(router.basename, ''); + }, -export { history }; + getSearch: (): string => { + return router.state.location.search; + } +}; + +export { router, location }; export enum URLParam { AGGREGATOR = 'aggregator', @@ -97,15 +111,17 @@ export enum ParamAction { export class HistoryManager { static setParam = (name: URLParam | string, value: string): void => { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); urlParams.set(name, value); - history.replace(`${history.location.pathname}?${urlParams.toString()}`); + + router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true }); }; static getParam = (name: URLParam | string, urlParams?: URLSearchParams): string | undefined => { if (!urlParams) { - urlParams = new URLSearchParams(history.location.search); + urlParams = new URLSearchParams(location.getSearch()); } + const p = urlParams.get(name); return p !== null ? p : undefined; }; @@ -120,73 +136,54 @@ export class HistoryManager { return p !== undefined ? p === 'true' : undefined; }; - static deleteParam = (name: URLParam, historyReplace?: boolean): void => { - const urlParams = new URLSearchParams(history.location.search); + static deleteParam = (name: URLParam): void => { + const urlParams = new URLSearchParams(location.getSearch()); urlParams.delete(name); - if (historyReplace) { - history.replace(`${history.location.pathname}?${urlParams.toString()}`); - } else { - history.push(`${history.location.pathname}?${urlParams.toString()}`); - } - }; - static setParams = (params: URLParamValue[], paramAction?: ParamAction, historyReplace?: boolean): void => { - const urlParams = new URLSearchParams(history.location.search); - - if (params.length > 0 && paramAction === ParamAction.APPEND) { - params.forEach(param => urlParams.delete(param.name)); - } - - params.forEach(param => { - if (param.value === '') { - urlParams.delete(param.name); - } else if (paramAction === ParamAction.APPEND) { - urlParams.append(param.name, param.value); - } else { - urlParams.set(param.name, param.value); - } - }); - - if (historyReplace) { - history.replace(`${history.location.pathname}?${urlParams.toString()}`); - } else { - history.push(`${history.location.pathname}?${urlParams.toString()}`); - } + router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true }); }; static getClusterName = (urlParams?: URLSearchParams): string | undefined => { if (!urlParams) { - urlParams = new URLSearchParams(history.location.search); + urlParams = new URLSearchParams(location.getSearch()); } + return urlParams.get(URLParam.CLUSTERNAME) || undefined; }; static getDuration = (urlParams?: URLSearchParams): number | undefined => { const duration = HistoryManager.getNumericParam(URLParam.DURATION, urlParams); + if (duration) { return toValidDuration(Number(duration)); } + return undefined; }; static getRangeDuration = (urlParams?: URLSearchParams): number | undefined => { const rangeDuration = HistoryManager.getNumericParam(URLParam.RANGE_DURATION, urlParams); + if (rangeDuration) { return toValidDuration(Number(rangeDuration)); } + return undefined; }; static getTimeBounds = (urlParams?: URLSearchParams): BoundsInMilliseconds | undefined => { const from = HistoryManager.getNumericParam(URLParam.FROM, urlParams); + if (from) { const to = HistoryManager.getNumericParam(URLParam.TO, urlParams); + // "to" can be undefined (stands for "now") return { from: from, to: to }; } + return undefined; }; } diff --git a/plugin/src/kiali/components/About/AboutUIModal.tsx b/plugin/src/kiali/components/About/AboutUIModal.tsx index 6e719409..1ddbb161 100644 --- a/plugin/src/kiali/components/About/AboutUIModal.tsx +++ b/plugin/src/kiali/components/About/AboutUIModal.tsx @@ -16,7 +16,7 @@ import { config, kialiLogoDark } from '../../config'; import { kialiStyle } from 'styles/StyleUtils'; import { KialiIcon } from 'config/KialiIcon'; import { ReactComponent as IstioLogo } from '../../assets/img/mesh/istio.svg'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import { PFColors } from 'components/Pf/PfColors'; type AboutUIModalProps = { diff --git a/plugin/src/kiali/components/BreadcrumbView/BreadcrumbView.tsx b/plugin/src/kiali/components/BreadcrumbView/BreadcrumbView.tsx index 4dee8d99..a129a09b 100644 --- a/plugin/src/kiali/components/BreadcrumbView/BreadcrumbView.tsx +++ b/plugin/src/kiali/components/BreadcrumbView/BreadcrumbView.tsx @@ -1,131 +1,105 @@ import * as React from 'react'; import { isMultiCluster, Paths } from '../../config'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom-v5-compat'; import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; import { FilterSelected } from '../Filters/StatefulFilters'; import { dicIstioType } from '../../types/IstioConfigList'; import { HistoryManager } from '../../app/History'; +import { useKialiTranslation } from 'utils/I18nUtils'; -interface BreadCumbViewProps { - location: { - pathname: string; - search: string; - }; -} - -interface BreadcrumbViewState { - cluster?: string; - istioType?: string; - item: string; - itemName: string; - namespace: string; - pathItem: string; -} - -const ItemNames = { - applications: 'App', - services: 'Service', - workloads: 'Workload', - istio: 'Istio Object' +const istioName = 'Istio Config'; +const namespaceRegex = /namespaces\/([a-z0-9-]+)\/([\w-.]+)\/([\w-.*]+)(\/([\w-.]+))?(\/([\w-.]+))?/; + +const capitalize = (str: string): string => { + return str.charAt(0).toUpperCase() + str.slice(1); }; -const IstioName = 'Istio Config'; -const namespaceRegex = /namespaces\/([a-z0-9-]+)\/([\w-.]+)\/([\w-.*]+)(\/([\w-.]+))?(\/([\w-.]+))?/; +const getIstioType = (rawType: string): string => { + const istioType = Object.keys(dicIstioType).find(key => dicIstioType[key] === rawType); + return istioType ? istioType : capitalize(rawType); +}; -export class BreadcrumbView extends React.Component { - static capitalize = (str: string) => { - return str.charAt(0).toUpperCase() + str.slice(1); - }; +const cleanFilters = (): void => { + FilterSelected.resetFilters(); +}; - static istioType(rawType: string) { - const istioType = Object.keys(dicIstioType).find(key => dicIstioType[key] === rawType); - return istioType ? istioType : this.capitalize(rawType); - } +export const BreadcrumbView: React.FC = () => { + const [cluster, setCluster] = React.useState(); + const [istioType, setIstioType] = React.useState(); + const [item, setItem] = React.useState(''); + const [namespace, setNamespace] = React.useState(''); + const [pathItem, setPathItem] = React.useState(''); - constructor(props: BreadCumbViewProps) { - super(props); - this.state = this.updateItem(); - } + const { pathname, search } = useLocation(); + const { t } = useKialiTranslation(); - updateItem = (): BreadcrumbViewState => { - const match = this.props.location.pathname.match(namespaceRegex) || []; + React.useEffect(() => { + const match = pathname.match(namespaceRegex) ?? []; const ns = match[1]; const page = Paths[match[2].toUpperCase()]; const istioType = match[3]; - const urlParams = new URLSearchParams(this.props.location.search); - let itemName = page !== 'istio' ? match[3] : match[5]; - return { - cluster: HistoryManager.getClusterName(urlParams), - istioType: istioType, - item: itemName, - itemName: ItemNames[page], - namespace: ns, - pathItem: page - }; - }; + const urlParams = new URLSearchParams(search); + const itemPage = page !== 'istio' ? match[3] : match[5]; - componentDidUpdate( - prevProps: Readonly, - _prevState: Readonly, - _snapshot?: any - ): void { - if (prevProps.location !== this.props.location) { - this.setState(this.updateItem()); - } - } + setCluster(HistoryManager.getClusterName(urlParams)); + setIstioType(istioType); + setItem(itemPage); + setNamespace(ns); + setPathItem(page); + }, [pathname, search]); - cleanFilters = () => { - FilterSelected.resetFilters(); + const isIstioPath = (): boolean => { + return pathItem === 'istio'; }; - isIstio = () => { - return this.state.pathItem === 'istio'; - }; + const getItemPage = (): string => { + let path = `/namespaces/${namespace}/${pathItem}/${item}`; - getItemPage = () => { - let path = `/namespaces/${this.state.namespace}/${this.state.pathItem}/${this.state.item}`; - if (this.state.cluster && isMultiCluster) { - path += `?clusterName=${this.state.cluster}`; + if (cluster && isMultiCluster) { + path += `?clusterName=${cluster}`; } + return path; }; - render() { - const { namespace, item, istioType, pathItem } = this.state; - const isIstio = this.isIstio(); - const linkItem = isIstio ? ( - {item} - ) : ( - - - {item} + const isIstio = isIstioPath(); + + const linkItem = isIstio ? ( + {item} + ) : ( + + + {item} + + + ); + + return ( + + + + {isIstio ? istioName : capitalize(pathItem)} - ); - return ( - - - - {isIstio ? IstioName : BreadcrumbView.capitalize(pathItem)} - - + + + + {t('Namespace: {{namespace}}', { namespace })} + + + + {isIstio && ( - - Namespace: {namespace} + + {istioType ? getIstioType(istioType) : istioType} - {isIstio && ( - - - {istioType ? BreadcrumbView.istioType(istioType) : istioType} - - - )} - {linkItem} - - ); - } -} + )} + + {linkItem} + + ); +}; diff --git a/plugin/src/kiali/components/CytoscapeGraph/ContextMenu/EdgeContextMenu.tsx b/plugin/src/kiali/components/CytoscapeGraph/ContextMenu/EdgeContextMenu.tsx index 128c0eda..d12a4a14 100644 --- a/plugin/src/kiali/components/CytoscapeGraph/ContextMenu/EdgeContextMenu.tsx +++ b/plugin/src/kiali/components/CytoscapeGraph/ContextMenu/EdgeContextMenu.tsx @@ -12,14 +12,12 @@ const contextMenu = kialiStyle({ textAlign: 'left' }); -export class EdgeContextMenu extends React.PureComponent { - render() { - return ( -
- {getTitle(`Edge (${prettyProtocol(this.props.protocol)})`)} - {renderBadgedName(decoratedNodeData((this.props.element as EdgeSingular).source()), 'From: ')} - {renderBadgedName(decoratedNodeData((this.props.element as EdgeSingular).target()), 'To: ')} -
- ); - } -} +export const EdgeContextMenu: React.FC = (props: EdgeContextMenuProps) => { + return ( +
+ {getTitle(`Edge (${prettyProtocol(props.protocol)})`)} + {renderBadgedName(decoratedNodeData((props.element as EdgeSingular).source()), 'From: ')} + {renderBadgedName(decoratedNodeData((props.element as EdgeSingular).target()), 'To: ')} +
+ ); +}; diff --git a/plugin/src/kiali/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx b/plugin/src/kiali/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx index 0991d882..4fb7f07f 100644 --- a/plugin/src/kiali/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx +++ b/plugin/src/kiali/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; import { kialiStyle } from 'styles/StyleUtils'; import { Spinner, Tooltip, TooltipPosition } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; -import { history } from 'app/History'; +import { router } from 'app/History'; import { BoxByType, DecoratedGraphNodeData, NodeType } from 'types/Graph'; import { TracingInfo } from 'types/TracingInfo'; import { durationSelector } from 'store/Selectors'; @@ -22,6 +21,7 @@ import { DurationInSeconds, TimeInMilliseconds } from 'types/Common'; import { useServiceDetailForGraphNode } from '../../../hooks/services'; import { canDelete } from '../../../types/Permissions'; import { getServiceDetailsUpdateLabel, hasServiceDetailsTrafficRouting } from '../../../types/ServiceInfo'; +import { useKialiTranslation } from 'utils/I18nUtils'; type ReduxProps = { duration: DurationInSeconds; @@ -38,15 +38,15 @@ const contextMenu = kialiStyle({ const contextMenuHeader = kialiStyle({ fontSize: 'var(--graph-side-panel--font-size)', - marginBottom: '3px', + marginBottom: '0.25rem', textAlign: 'left' }); const contextMenuSubTitle = kialiStyle({ color: PFColors.Color200, fontWeight: 700, - paddingTop: 2, - paddingBottom: 4 + paddingTop: '0.125rem', + paddingBottom: '0.25rem' }); const contextMenuItem = kialiStyle({ @@ -66,22 +66,25 @@ const contextMenuItemLink = kialiStyle({ const hrStyle = kialiStyle({ border: 0, borderTop: `1px solid ${PFColors.BorderColor100}`, - margin: '8px 0 5px 0' + margin: '0.5rem 0 0.25rem 0' }); type Props = NodeContextMenuProps & ReduxProps; type LinkParams = { cluster?: string; name: string; namespace: string; type: string }; -function getLinkParamsForNode(node: DecoratedGraphNodeData): LinkParams | undefined { - let cluster = node.cluster; +const getLinkParamsForNode = (node: DecoratedGraphNodeData): LinkParams | undefined => { const namespace: string = node.isServiceEntry ? node.isServiceEntry.namespace : node.namespace; + + let cluster = node.cluster; let name: string | undefined = undefined; let type: string | undefined = undefined; + switch (node.nodeType) { case NodeType.APP: case NodeType.BOX: // only app boxes have full context menus const isBox = node.isBox; + if (!isBox || isBox === BoxByType.APP) { // Prefer workload links if (node.workload && node.parent) { @@ -104,36 +107,40 @@ function getLinkParamsForNode(node: DecoratedGraphNodeData): LinkParams | undefi } return type && name ? { namespace, type, name, cluster } : undefined; -} +}; + +export const NodeContextMenuComponent: React.FC = (props: Props) => { + const { t } = useKialiTranslation(); -export function NodeContextMenuComponent(props: Props): React.ReactElement | null { const [serviceDetails, gateways, peerAuthentications, isServiceDetailsLoading] = useServiceDetailForGraphNode( props, !props.isInaccessible, props.duration, props.updateTime ); + const updateLabel = getServiceDetailsUpdateLabel(serviceDetails); // TODO: Deduplicate - function getDropdownItemTooltipMessage(): string { + const getDropdownItemTooltipMessage = (): string => { if (serverConfig.deployment.viewOnlyMode) { - return 'User does not have permission'; + return t('User does not have permission'); } else if (hasServiceDetailsTrafficRouting(serviceDetails)) { - return 'Traffic routing already exists for this service'; + return t('Traffic routing already exists for this service'); } else { - return "Traffic routing doesn't exists for this service"; + return t("Traffic routing doesn't exist for this service"); } - } + }; - function createMenuItem(href: string, title: string, target = '_self', external = false): React.ReactElement { + const createMenuItem = (href: string, title: string, target = '_self', external = false): React.ReactNode => { const commonLinkProps = { className: contextMenuItemLink, children: title, - onClick: onClick, target }; + let item: any; + if (external) { item = ( @@ -145,8 +152,7 @@ export function NodeContextMenuComponent(props: Props): React.ReactElement | nul // otherwise the kiosk=true will keep the links inside Kiali if (isParentKiosk(props.kiosk)) { item = ( - { kioskContextMenuAction(href); }} @@ -155,7 +161,7 @@ export function NodeContextMenuComponent(props: Props): React.ReactElement | nul /> ); } else { - item = ; + item = onClick(href)} />; } } @@ -164,13 +170,14 @@ export function NodeContextMenuComponent(props: Props): React.ReactElement | nul {item} ); - } + }; - function onClick(_e: React.MouseEvent): void { + const onClick = (href: string): void => { props.contextMenu.hide(0); - } + router.navigate(href); + }; - function handleClickWizard(e: React.MouseEvent, eventKey: WizardAction): void { + const handleClickWizard = (e: React.MouseEvent, eventKey: WizardAction): void => { e.preventDefault(); props.contextMenu.hide(0); @@ -184,33 +191,35 @@ export function NodeContextMenuComponent(props: Props): React.ReactElement | nul peerAuthentications ); } - } + }; - function handleDeleteTrafficRouting(e: React.MouseEvent): void { + const handleDeleteTrafficRouting = (e: React.MouseEvent): void => { e.preventDefault(); props.contextMenu.hide(0); if (props.onDeleteTrafficRouting && serviceDetails) { props.onDeleteTrafficRouting(DELETE_TRAFFIC_ROUTING, serviceDetails); } - } + }; - function renderHeader(): React.ReactElement { + const renderHeader = (): React.ReactNode => { return ( <> {props.isBox ? getTitle(props.isBox) : getTitle(props.nodeType)} + {(!props.isBox || props.isBox === BoxByType.APP) && (
{props.namespace}
)} + {renderBadgedName(props)} ); - } + }; - function renderWizardActionItem(eventKey: string): React.ReactElement { + const renderWizardActionItem = (eventKey: string): React.ReactNode => { const enabledItem = !hasServiceDetailsTrafficRouting(serviceDetails) || (hasServiceDetailsTrafficRouting(serviceDetails) && updateLabel === eventKey); @@ -241,9 +250,9 @@ export function NodeContextMenuComponent(props: Props): React.ReactElement | nul ); } - } + }; - function renderDeleteTrafficRoutingItem(): React.ReactElement { + const renderDeleteTrafficRoutingItem = (): React.ReactNode => { if ( !canDelete(serviceDetails?.istioPermissions) || !hasServiceDetailsTrafficRouting(serviceDetails) /*|| props.isDisabled*/ @@ -270,16 +279,16 @@ export function NodeContextMenuComponent(props: Props): React.ReactElement | nul ); } - } + }; - function renderWizardsItems(): React.ReactElement | null { + const renderWizardsItems = (): React.ReactNode | null => { if (isServiceDetailsLoading) { return ( <>
Actions
- +
); @@ -289,7 +298,7 @@ export function NodeContextMenuComponent(props: Props): React.ReactElement | nul return ( <>
-
{updateLabel === '' ? 'Create' : 'Update'}
+
{updateLabel === '' ? t('Create') : t('Update')}
{SERVICE_WIZARD_ACTIONS.map(eventKey => renderWizardActionItem(eventKey))}
{renderDeleteTrafficRoutingItem()} @@ -298,31 +307,7 @@ export function NodeContextMenuComponent(props: Props): React.ReactElement | nul } return null; - } - - function renderFullContextMenu(linkParams: LinkParams): React.ReactElement { - // The getOptionsFromLinkParams function can potentially return a blank list if the - // node associated to the context menu is for a remote cluster with no accessible Kialis. - // That would lead to an empty menu. Here, we assume that whoever is the host/parent component, - // that component won't render this context menu in case this menu would be blank. So, here - // it's simply assumed that the context menu will look good. - const options: ContextMenuOption[] = getOptionsFromLinkParams(linkParams, props.tracingInfo); - const menuOptions = ( - <> -
Show
- {options.map(o => createMenuItem(o.url, o.text, o.target, o.external))} - - ); - - return ( -
- {renderHeader()} -
- {menuOptions} - {renderWizardsItems()} -
- ); - } + }; if (props.isInaccessible) { props.contextMenu.disable(); @@ -341,8 +326,28 @@ export function NodeContextMenuComponent(props: Props): React.ReactElement | nul return null; } - return renderFullContextMenu(linkParams); -} + // The getOptionsFromLinkParams function can potentially return a blank list if the + // node associated to the context menu is for a remote cluster with no accessible Kialis. + // That would lead to an empty menu. Here, we assume that whoever is the host/parent component, + // that component won't render this context menu in case this menu would be blank. So, here + // it's simply assumed that the context menu will look good. + const options: ContextMenuOption[] = getOptionsFromLinkParams(linkParams, props.tracingInfo); + const menuOptions = ( + <> +
{t('Show')}
+ {options.map(o => createMenuItem(o.url, o.text, o.target, o.external))} + + ); + + return ( +
+ {renderHeader()} +
+ {menuOptions} + {renderWizardsItems()} +
+ ); +}; const getTracingURL = (namespace: string, namespaceSelector: boolean, tracingURL: string, name?: string): string => { return `${tracingURL}/search?service=${name}${namespaceSelector ? `.${namespace}` : ''}`; @@ -362,7 +367,7 @@ export const clickHandler = (o: ContextMenuOption, kiosk: string): void => { if (isParentKiosk(kiosk)) { kioskContextMenuAction(o.url); } else { - history.push(o.url); + router.navigate(o.url); } } }; @@ -371,36 +376,46 @@ export const getOptions = (node: DecoratedGraphNodeData, tracingInfo?: TracingIn if (node.isInaccessible) { return []; } + const linkParams = getLinkParamsForNode(node); + if (!linkParams) { return []; } + return getOptionsFromLinkParams(linkParams, tracingInfo); }; const getOptionsFromLinkParams = (linkParams: LinkParams, tracingInfo?: TracingInfo): ContextMenuOption[] => { - let options: ContextMenuOption[] = []; const { namespace, type, name, cluster } = linkParams; + + let options: ContextMenuOption[] = []; let detailsPageUrl = `/namespaces/${namespace}/${type}/${name}`; let concat = '?'; + if (cluster && isMultiCluster) { detailsPageUrl += `?clusterName=${cluster}`; concat = '&'; } options.push({ text: 'Details', url: detailsPageUrl }); + if (type !== Paths.SERVICEENTRIES) { options.push({ text: 'Traffic', url: `${detailsPageUrl}${concat}tab=traffic` }); + if (type === Paths.WORKLOADS) { options.push({ text: 'Logs', url: `${detailsPageUrl}${concat}tab=logs` }); } + options.push({ text: 'Inbound Metrics', url: `${detailsPageUrl}${concat}tab=${type === Paths.SERVICES ? 'metrics' : 'in_metrics'}` }); + if (type !== Paths.SERVICES) { options.push({ text: 'Outbound Metrics', url: `${detailsPageUrl}${concat}tab=out_metrics` }); } + if (type === Paths.APPLICATIONS && tracingInfo && tracingInfo.enabled) { if (tracingInfo.integration) { options.push({ text: 'Traces', url: `${detailsPageUrl}${concat}tab=traces` }); diff --git a/plugin/src/kiali/components/CytoscapeGraph/CytoscapeContextMenu.tsx b/plugin/src/kiali/components/CytoscapeGraph/CytoscapeContextMenu.tsx index eb3971b1..f561d8a3 100644 --- a/plugin/src/kiali/components/CytoscapeGraph/CytoscapeContextMenu.tsx +++ b/plugin/src/kiali/components/CytoscapeGraph/CytoscapeContextMenu.tsx @@ -1,17 +1,18 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as Cy from 'cytoscape'; -import { Router } from 'react-router'; +import { RouterProvider } from 'react-router-dom-v5-compat'; import tippy, { Instance } from 'tippy.js'; import { DecoratedGraphEdgeData, DecoratedGraphNodeData } from '../../types/Graph'; import { PeerAuthentication } from '../../types/IstioObjects'; import { ServiceDetailsInfo } from '../../types/ServiceInfo'; import { Provider } from 'react-redux'; import { store } from '../../store/ConfigStore'; -import { history } from '../../app/History'; +import { createRouter } from '../../app/History'; import { getOptions } from './ContextMenu/NodeContextMenu'; import { WizardAction, WizardMode } from '../IstioWizards/WizardActions'; import { Theme } from 'types/Common'; +import { pathRoutes } from 'routes'; export type EdgeContextMenuProps = DecoratedGraphEdgeData & ContextMenuProps; export type EdgeContextMenuComponentType = React.ComponentType; @@ -75,7 +76,7 @@ export class CytoscapeContextMenuWrapper extends React.PureComponent { document.removeEventListener('mouseup', this.handleDocumentMouseUp); } - render(): React.ReactElement { + render(): React.ReactNode { return (
@@ -84,27 +85,30 @@ export class CytoscapeContextMenuWrapper extends React.PureComponent { } // Add cy listener for context menu events on nodes and edges - connectCy(cy: Cy.Core): void { + connectCy = (cy: Cy.Core): void => { cy.on('cxttapstart', 'node,edge', (event: Cy.EventObject) => { event.preventDefault(); + if (event.target) { this.handleContextMenu(event.target, false); } + return false; }); - } + }; // Connects cy to this component - handleContextMenu(elem: Cy.NodeSingular | Cy.EdgeSingular, isHover: boolean): void { + handleContextMenu = (elem: Cy.NodeSingular | Cy.EdgeSingular, isHover: boolean): void => { const contextMenuType = elem.isNode() ? this.props.contextMenuNodeComponent : this.props.contextMenuEdgeComponent; if (contextMenuType) { this.makeContextMenu(contextMenuType, elem, isHover, elem.isNode()); } - } + }; - hideContextMenu(isHover: boolean | undefined): void { + hideContextMenu = (isHover: boolean | undefined): void => { const currentContextMenu = this.getCurrentContextMenu(); + if (currentContextMenu) { if (!isHover || this.isHover) { currentContextMenu.hide(0); // hide it in 0ms @@ -112,14 +116,16 @@ export class CytoscapeContextMenuWrapper extends React.PureComponent { ReactDOM.unmountComponentAtNode(this.contextMenuRef.current as HTMLDivElement); } } - } + }; private handleDocumentMouseUp = (event: MouseEvent): void => { if (event.button === 2) { // Ignore mouseup of right button return; } + const currentContextMenu: Instance | undefined = this.getCurrentContextMenu(); + if (currentContextMenu) { // Allow interaction in our popper component (Selecting and copying) without it disappearing if (event.target && currentContextMenu.popper.contains(event.target as Node)) { @@ -130,12 +136,12 @@ export class CytoscapeContextMenuWrapper extends React.PureComponent { } }; - private makeContextMenu( + private makeContextMenu = ( ContextMenuComponentType: ContextMenuComponentType, target: Cy.NodeSingular | Cy.EdgeSingular, isHover: boolean, isNode: boolean - ): void { + ): void => { // Don't let a hover trump a non-hover context menu if (isHover && this.isHover === false) { return; @@ -151,6 +157,7 @@ export class CytoscapeContextMenuWrapper extends React.PureComponent { // Prevent the tippy content from picking up the right-click when we are moving it over to the edge/node this.addContextMenuEventListener(); const content = this.contextMenuRef.current; + const tippyInstance = tippy( (target as any).popperRef(), // Using an extension, popperRef is not in base definition { @@ -171,6 +178,7 @@ export class CytoscapeContextMenuWrapper extends React.PureComponent { let menuComponent = ( ); + if (isNode) { menuComponent = ( { ); } + const contextMenuRouter = createRouter([ + { + element: menuComponent, + children: pathRoutes + } + ]); + const result = ( - {menuComponent} + ); @@ -196,42 +211,45 @@ export class CytoscapeContextMenuWrapper extends React.PureComponent { ReactDOM.render(result, content, () => { this.setCurrentContextMenu(tippyInstance); tippyInstance.show(); + // Schedule the removal of the contextmenu listener after finishing with the show procedure, so we can // interact with the popper content e.g. select and copy (with right click) values from it. setTimeout(() => { this.removeContextMenuEventListener(); }, 0); }); - } + }; - private getCurrentContextMenu(): Instance | undefined { + private getCurrentContextMenu = (): Instance | undefined => { return this.contextMenuRef?.current?._contextMenu; - } + }; - private setCurrentContextMenu(current: TippyInstance): void { + private setCurrentContextMenu = (current: TippyInstance): void => { this.contextMenuRef!.current!._contextMenu = current; - } + }; - private addContextMenuEventListener(): void { + private addContextMenuEventListener = (): void => { document.addEventListener('contextmenu', this.handleContextMenuEvent); - } + }; - private removeContextMenuEventListener(): void { + private removeContextMenuEventListener = (): void => { document.removeEventListener('contextmenu', this.handleContextMenuEvent); - } + }; private handleContextMenuEvent = (event: MouseEvent): boolean => { // Disable the context menu in popper const currentContextMenu = this.getCurrentContextMenu(); + if (currentContextMenu) { if (event.target && currentContextMenu.popper.contains(event.target as Node)) { event.preventDefault(); } } + return true; }; - private tippyDistance(_target: Cy.NodeSingular | Cy.EdgeSingular): number { + private tippyDistance = (_target: Cy.NodeSingular | Cy.EdgeSingular): number => { return 10; - } + }; } diff --git a/plugin/src/kiali/components/CytoscapeGraph/MiniGraphCard.tsx b/plugin/src/kiali/components/CytoscapeGraph/MiniGraphCard.tsx index 10c16d57..b3a11e17 100644 --- a/plugin/src/kiali/components/CytoscapeGraph/MiniGraphCard.tsx +++ b/plugin/src/kiali/components/CytoscapeGraph/MiniGraphCard.tsx @@ -12,7 +12,7 @@ import { MenuToggleElement, ToolbarItem } from '@patternfly/react-core'; -import { history } from '../../app/History'; +import { router } from '../../app/History'; import { GraphDataSource } from '../../services/GraphDataSource'; import { DecoratedGraphElements, EdgeMode, GraphType, NodeType } from '../../types/Graph'; import { CytoscapeGraph, GraphEdgeTapEvent, GraphNodeTapEvent } from './CytoscapeGraph'; @@ -157,6 +157,7 @@ class MiniGraphCardComponent extends React.Component {intervalTitle} +
+ { this.cytoscapeGraphRef.current = cytoscapeGraph; - } + }; private handleLaunchWizard = (key: WizardAction, mode: WizardMode): void => { this.onGraphActionsToggle(false); @@ -236,6 +238,7 @@ class MiniGraphCardComponent extends React.Component { diff --git a/plugin/src/kiali/components/DebugInformation/DebugInformation.tsx b/plugin/src/kiali/components/DebugInformation/DebugInformation.tsx index db4cd778..ca7716e8 100644 --- a/plugin/src/kiali/components/DebugInformation/DebugInformation.tsx +++ b/plugin/src/kiali/components/DebugInformation/DebugInformation.tsx @@ -128,20 +128,22 @@ const DebugInformationComponent: React.FC = (props: Debug setConfig(kialiConfig); }, []); - const prevAppState = usePreviousValue(props.appState); + const { appState, isOpen } = props; + + const prevAppState = usePreviousValue(appState); React.useEffect(() => { - if (prevAppState !== props.appState && copyStatus === CopyStatus.COPIED) { + if (prevAppState !== appState && copyStatus === CopyStatus.COPIED) { setCopyStatus(CopyStatus.OLD_COPY); } - }, [prevAppState, props.appState, copyStatus]); + }, [prevAppState, appState, copyStatus]); React.useEffect(() => { - if (props.isOpen) { + if (isOpen) { setCopyStatus(CopyStatus.NOT_COPIED); setCurrentTab(defaultTab); } - }, [props.isOpen]); + }, [isOpen]); const copyCallback = (_text: string, result: boolean): void => { setCopyStatus(result ? CopyStatus.COPIED : CopyStatus.NOT_COPIED); diff --git a/plugin/src/kiali/components/DefaultSecondaryMasthead/DefaultSecondaryMasthead.tsx b/plugin/src/kiali/components/DefaultSecondaryMasthead/DefaultSecondaryMasthead.tsx index 74312237..4723e519 100644 --- a/plugin/src/kiali/components/DefaultSecondaryMasthead/DefaultSecondaryMasthead.tsx +++ b/plugin/src/kiali/components/DefaultSecondaryMasthead/DefaultSecondaryMasthead.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Title, TitleSizes } from '@patternfly/react-core'; import { NamespaceDropdown } from '../Dropdown/NamespaceDropdown'; import { kialiStyle } from 'styles/StyleUtils'; diff --git a/plugin/src/kiali/components/DetailDescription/DetailDescription.tsx b/plugin/src/kiali/components/DetailDescription/DetailDescription.tsx index 465f9850..d305604b 100644 --- a/plugin/src/kiali/components/DetailDescription/DetailDescription.tsx +++ b/plugin/src/kiali/components/DetailDescription/DetailDescription.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { AppWorkload } from '../../types/App'; import { PopoverPosition, Tooltip, TooltipPosition } from '@patternfly/react-core'; import { kialiStyle } from 'styles/StyleUtils'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import { MissingSidecar } from '../MissingSidecar/MissingSidecar'; import * as H from '../../types/Health'; import { HealthSubItem } from '../../types/Health'; diff --git a/plugin/src/kiali/components/Dropdown/DurationDropdown.tsx b/plugin/src/kiali/components/Dropdown/DurationDropdown.tsx index 65173bf9..2dfae209 100644 --- a/plugin/src/kiali/components/Dropdown/DurationDropdown.tsx +++ b/plugin/src/kiali/components/Dropdown/DurationDropdown.tsx @@ -8,29 +8,32 @@ import { KialiDispatch } from 'types/Redux'; import { bindActionCreators } from 'redux'; import { UserSettingsActions } from '../../actions/UserSettingsActions'; import { connect } from 'react-redux'; -import { HistoryManager, URLParam } from '../../app/History'; -import { history } from '../../app/History'; +import { HistoryManager, URLParam, location } from '../../app/History'; import { TooltipPosition } from '@patternfly/react-core'; import { isKioskMode } from '../../utils/SearchParamUtils'; import { kioskDurationAction } from '../Kiosk/KioskActions'; -type ReduxProps = { +type ReduxStateProps = { duration: DurationInSeconds; - setDuration: (duration: DurationInSeconds) => void; }; -type DurationDropdownProps = ReduxProps & { - disabled?: boolean; - id: string; - nameDropdown?: string; - prefix?: string; - suffix?: string; - tooltip?: string; - tooltipPosition?: TooltipPosition; +type ReduxDispatchProps = { + setDuration: (duration: DurationInSeconds) => void; }; +type DurationDropdownProps = ReduxStateProps & + ReduxDispatchProps & { + disabled?: boolean; + id: string; + nameDropdown?: string; + prefix?: string; + suffix?: string; + tooltip?: string; + tooltipPosition?: TooltipPosition; + }; + export const DurationDropdownComponent: React.FC = (props: DurationDropdownProps) => { - const updateDurationInterval = (duration: number) => { + const updateDurationInterval = (duration: number): void => { props.setDuration(duration); // notify redux of the change if (isKioskMode()) { @@ -55,33 +58,37 @@ export const DurationDropdownComponent: React.FC = (props ); }; -const withURLAwareness = (DurationDropdownComponent: React.FC) => { +const withURLAwareness = ( + DurationDropdownComponent: React.FC +): React.ComponentClass => { return class extends React.Component { constructor(props: DurationDropdownProps) { super(props); - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); const urlDuration = HistoryManager.getDuration(urlParams); + if (urlDuration !== undefined && urlDuration !== props.duration) { props.setDuration(urlDuration); } + HistoryManager.setParam(URLParam.DURATION, String(props.duration)); } - componentDidUpdate() { + componentDidUpdate(): void { HistoryManager.setParam(URLParam.DURATION, String(this.props.duration)); } - render() { + render(): React.ReactNode { return ; } }; }; -const mapStateToProps = (state: KialiAppState) => ({ +const mapStateToProps = (state: KialiAppState): ReduxStateProps => ({ duration: durationSelector(state) }); -const mapDispatchToProps = (dispatch: KialiDispatch) => { +const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => { return { setDuration: bindActionCreators(UserSettingsActions.setDuration, dispatch) }; diff --git a/plugin/src/kiali/components/Envoy/EnvoyDetails.tsx b/plugin/src/kiali/components/Envoy/EnvoyDetails.tsx index 34789553..494cfbfd 100644 --- a/plugin/src/kiali/components/Envoy/EnvoyDetails.tsx +++ b/plugin/src/kiali/components/Envoy/EnvoyDetails.tsx @@ -34,7 +34,7 @@ import { DashboardRef } from 'types/Runtimes'; import { CustomMetrics } from 'components/Metrics/CustomMetrics'; import { serverConfig } from 'config'; import { FilterSelected } from 'components/Filters/StatefulFilters'; -import { history } from '../../app/History'; +import { location, router } from '../../app/History'; import { tabName as workloadTabName, defaultTab as workloadDefaultTab @@ -153,11 +153,11 @@ class EnvoyDetailsComponent extends React.Component = (props: MessageProps) => { - const alert = (): React.ReactNode => { - return ( - - - - {' '} - - - - ); - }; - - return {props.children}; -}; diff --git a/plugin/src/kiali/components/FilterList/FilterComponent.tsx b/plugin/src/kiali/components/FilterList/FilterComponent.tsx index 1e400a38..341fea25 100644 --- a/plugin/src/kiali/components/FilterList/FilterComponent.tsx +++ b/plugin/src/kiali/components/FilterList/FilterComponent.tsx @@ -16,7 +16,7 @@ export interface State { listItems: R[]; } -export abstract class Component

, S extends State, R> extends React.Component { +export abstract class Component, R> extends React.Component { abstract sortItemList(listItems: R[], sortField: SortField, isAscending: boolean): R[]; abstract updateListItems(resetPagination?: boolean): void; diff --git a/plugin/src/kiali/components/FilterList/FilterHelper.ts b/plugin/src/kiali/components/FilterList/FilterHelper.ts index a4b62f47..714136e6 100644 --- a/plugin/src/kiali/components/FilterList/FilterHelper.ts +++ b/plugin/src/kiali/components/FilterList/FilterHelper.ts @@ -1,5 +1,5 @@ import { camelCase } from 'lodash'; -import { history, URLParam, HistoryManager } from '../../app/History'; +import { URLParam, HistoryManager, router, location } from '../../app/History'; import { config } from '../../config'; import { ActiveFilter, @@ -17,12 +17,12 @@ export const perPageOptions: number[] = [5, 10, 15]; const defaultDuration = 600; const defaultRefreshInterval = config.toolbar.defaultRefreshInterval; -export const handleError = (error: string) => { +export const handleError = (error: string): void => { AlertUtils.add(error); }; export const getFiltersFromURL = (filterTypes: FilterType[]): ActiveFiltersInfo => { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); const activeFilters: ActiveFilter[] = []; filterTypes.forEach(filter => { urlParams.getAll(camelCase(filter.category)).forEach(value => { @@ -40,42 +40,51 @@ export const getFiltersFromURL = (filterTypes: FilterType[]): ActiveFiltersInfo }; export const setFiltersToURL = (filterTypes: FilterType[], filters: ActiveFiltersInfo): ActiveFiltersInfo => { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); + filterTypes.forEach(type => { urlParams.delete(camelCase(type.category)); }); + // Remove manually the special Filter opLabel urlParams.delete('opLabel'); const cleanFilters: ActiveFilter[] = []; filters.filters.forEach(activeFilter => { const filterType = filterTypes.find(filter => filter.category === activeFilter.category); + if (!filterType) { return; } + cleanFilters.push(activeFilter); urlParams.append(camelCase(filterType.category), activeFilter.value); }); + urlParams.append(ID_LABEL_OPERATION, filters.op); + // Resetting pagination when filters change - history.push(history.location.pathname + '?' + urlParams.toString()); + router.navigate(`${location.getPathname()}?${urlParams.toString()}`); return { filters: cleanFilters, op: filters.op || DEFAULT_LABEL_OPERATION }; }; export const filtersMatchURL = (filterTypes: FilterType[], filters: ActiveFiltersInfo): boolean => { // This can probably be improved and/or simplified? const fromFilters: Map = new Map(); + filters.filters.forEach(activeFilter => { const existingValue = fromFilters.get(activeFilter.category) || []; fromFilters.set(activeFilter.category, existingValue.concat(activeFilter.value)); }); const fromURL: Map = new Map(); - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); + filterTypes.forEach(filter => { const values = urlParams.getAll(camelCase(filter.category)); + if (values.length > 0) { - const existing = fromURL.get(camelCase(filter.category)) || []; + const existing = fromURL.get(camelCase(filter.category)) ?? []; fromURL.set(filter.category, existing.concat(values)); } }); @@ -83,9 +92,12 @@ export const filtersMatchURL = (filterTypes: FilterType[], filters: ActiveFilter if (fromFilters.size !== fromURL.size) { return false; } + let equalFilters = true; + fromFilters.forEach((filterValues, filterName) => { - const aux = fromURL.get(filterName) || []; + const aux = fromURL.get(filterName) ?? []; + equalFilters = equalFilters && filterValues.every(value => aux.includes(value)) && filterValues.length === aux.length; }); @@ -94,7 +106,7 @@ export const filtersMatchURL = (filterTypes: FilterType[], filters: ActiveFilter }; export const isCurrentSortAscending = (): boolean => { - return (HistoryManager.getParam(URLParam.DIRECTION) || 'asc') === 'asc'; + return (HistoryManager.getParam(URLParam.DIRECTION) ?? 'asc') === 'asc'; }; export const currentDuration = (): number => { @@ -103,18 +115,21 @@ export const currentDuration = (): number => { export const currentRefreshInterval = (): number => { const refreshInterval = HistoryManager.getNumericParam(URLParam.REFRESH_INTERVAL); + if (refreshInterval === undefined) { return defaultRefreshInterval; } + return refreshInterval; }; export const currentSortField = (sortFields: SortField[]): SortField => { const queriedSortedField = HistoryManager.getParam(URLParam.SORT) || sortFields[0].param; + return ( sortFields.find(sortField => { return sortField.param === queriedSortedField; - }) || sortFields[0] + }) ?? sortFields[0] ); }; @@ -128,14 +143,16 @@ export const compareNullable = (a: T | undefined, b: T | undefined, safeComp: return safeComp(a, b); }; -export const runFilters = (items: T[], filters: RunnableFilter[], active: ActiveFiltersInfo) => { +export const runFilters = (items: T[], filters: RunnableFilter[], active: ActiveFiltersInfo): T[] => { return filters.reduce((i, f) => runOneFilter(i, f, active), items); }; -const runOneFilter = (items: T[], filter: RunnableFilter, active: ActiveFiltersInfo) => { +const runOneFilter = (items: T[], filter: RunnableFilter, active: ActiveFiltersInfo): T[] => { const relatedActive = { filters: active.filters.filter(af => af.category === filter.category), op: active.op }; + if (relatedActive.filters.length) { return items.filter(item => filter.run(item, relatedActive)); } + return items; }; diff --git a/plugin/src/kiali/components/FilterList/__tests__/ListPagesHelper.test.ts b/plugin/src/kiali/components/FilterList/__tests__/ListPagesHelper.test.ts index d17a7b58..e0a47abe 100644 --- a/plugin/src/kiali/components/FilterList/__tests__/ListPagesHelper.test.ts +++ b/plugin/src/kiali/components/FilterList/__tests__/ListPagesHelper.test.ts @@ -1,4 +1,4 @@ -import { history } from '../../../app/History'; +import { location, router } from '../../../app/History'; import * as FilterHelper from '../FilterHelper'; import { DEFAULT_LABEL_OPERATION, FilterType } from '../../../types/Filters'; @@ -16,7 +16,7 @@ const managedFilterTypes = [ describe('List page', () => { it('sets selected filters from URL', () => { - history.push('?a=1&b=2&c=3&c=4'); + router.navigate('?a=1&b=2&c=3&c=4'); const filters = FilterHelper.getFiltersFromURL(managedFilterTypes); expect(filters).toEqual({ filters: [ @@ -38,7 +38,7 @@ describe('List page', () => { }); it('sets selected filters to URL', () => { - history.push('?a=10&b=20&c=30&c=40'); + router.navigate('?a=10&b=20&c=30&c=40'); const cleanFilters = FilterHelper.setFiltersToURL(managedFilterTypes, { filters: [ { @@ -56,12 +56,12 @@ describe('List page', () => { ], op: DEFAULT_LABEL_OPERATION }); - expect(history.location.search).toEqual('?b=20&a=1&c=3&c=4&opLabel=or'); + expect(location.getSearch()).toEqual('?b=20&a=1&c=3&c=4&opLabel=or'); expect(cleanFilters.filters).toHaveLength(3); }); it('sets selected filters to URL with OpLabel to and', () => { - history.push('?a=10&b=20&c=30&c=40'); + router.navigate('?a=10&b=20&c=30&c=40'); const cleanFilters = FilterHelper.setFiltersToURL(managedFilterTypes, { filters: [ { @@ -79,13 +79,13 @@ describe('List page', () => { ], op: 'and' }); - expect(history.location.search).toEqual('?b=20&a=1&c=3&c=4&opLabel=and'); + expect(location.getSearch()).toEqual('?b=20&a=1&c=3&c=4&opLabel=and'); expect(cleanFilters.filters).toHaveLength(3); expect(cleanFilters.op).toEqual('and'); }); it('filters should match URL, ignoring order and non-managed query params', () => { - history.push('?a=1&b=2&c=3&c=4'); + router.navigate('?a=1&b=2&c=3&c=4'); // Make sure order is ignored const match = FilterHelper.filtersMatchURL(managedFilterTypes, { filters: [ @@ -108,7 +108,7 @@ describe('List page', () => { }); it('filters should not match URL', () => { - history.push('?a=1&b=2&c=3&c=4'); + router.navigate('?a=1&b=2&c=3&c=4'); // Incorrect value let match = FilterHelper.filtersMatchURL(managedFilterTypes, { filters: [ diff --git a/plugin/src/kiali/components/Filters/StatefulFilters.tsx b/plugin/src/kiali/components/Filters/StatefulFilters.tsx index aea7ef09..a0c3a13d 100644 --- a/plugin/src/kiali/components/Filters/StatefulFilters.tsx +++ b/plugin/src/kiali/components/Filters/StatefulFilters.tsx @@ -35,7 +35,7 @@ import { kialiStyle } from 'styles/StyleUtils'; import { LabelFilters } from './LabelFilter'; import { arrayEquals } from 'utils/Common'; import { labelFilter } from './CommonFilters'; -import { history, HistoryManager } from 'app/History'; +import { HistoryManager, location } from 'app/History'; import { serverConfig } from 'config'; import { PFColors } from '../Pf/PfColors'; import { t } from 'utils/I18nUtils'; @@ -140,7 +140,7 @@ export class Toggles { Toggles.numChecked = 0; // Prefer URL settings - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); toggles.forEach(t => { const urlIsChecked = HistoryManager.getBooleanParam(`${t.name}Toggle`, urlParams); @@ -438,7 +438,7 @@ export class StatefulFiltersComponent extends React.Component): React.ReactElement => ( + const typeaheadToggle = (toggleRef: React.Ref): React.ReactNode => ( ); } else if (currentFilterType.filterType === AllFilterTypes.select) { - const selectToggle = (toggleRef: React.Ref): React.ReactElement => ( + const selectToggle = (toggleRef: React.Ref): React.ReactNode => ( f.category === labelFilter.category) || this.state.currentFilterType.filterType === AllFilterTypes.label; - const filterTypeToggle = (toggleRef: React.Ref): React.ReactElement => ( + const filterTypeToggle = (toggleRef: React.Ref): React.ReactNode => ( ); - const filterValueToggle = (toggleRef: React.Ref): React.ReactElement => ( + const filterValueToggle = (toggleRef: React.Ref): React.ReactNode => ( { const kiosk = useKialiSelector(state => state.globalState.kiosk); + const { t } = useKialiTranslation(); + const navigate = useNavigate(); const [dropdownOpen, setDropdownOpen] = React.useState(false); @@ -40,7 +43,7 @@ export const IstioActionsNamespaceDropdown: React.FC = () => { if (isParentKiosk(kiosk)) { kioskContextMenuAction(newUrl); } else { - history.push(newUrl); + navigate(newUrl); } }; @@ -65,7 +68,7 @@ export const IstioActionsNamespaceDropdown: React.FC = () => { const dropdownItems = [ r.action)} /> @@ -83,7 +86,7 @@ export const IstioActionsNamespaceDropdown: React.FC = () => { data-test="istio-actions-toggle" isExpanded={dropdownOpen} > - Actions + {t('Actions')} )} isOpen={dropdownOpen} diff --git a/plugin/src/kiali/components/IstioStatus/IstioStatus.tsx b/plugin/src/kiali/components/IstioStatus/IstioStatus.tsx index a10e6980..6db9d186 100644 --- a/plugin/src/kiali/components/IstioStatus/IstioStatus.tsx +++ b/plugin/src/kiali/components/IstioStatus/IstioStatus.tsx @@ -20,8 +20,9 @@ import { NamespaceThunkActions } from '../../actions/NamespaceThunkActions'; import { connectRefresh } from '../Refresh/connectRefresh'; import { kialiStyle } from 'styles/StyleUtils'; import { IconProps, createIcon } from 'config/KialiIcon'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import { useKialiTranslation } from 'utils/I18nUtils'; +import { MASTHEAD } from 'components/Nav/Masthead/Masthead'; type ReduxStateProps = { namespaces?: Namespace[]; @@ -67,8 +68,8 @@ const defaultIcons = { }; const iconStyle = kialiStyle({ - marginLeft: '0.5rem', - verticalAlign: '-0.125rem' + marginLeft: '2rem', + fontSize: '1rem' }); export const meshLinkStyle = kialiStyle({ @@ -116,6 +117,7 @@ export const IstioStatusComponent: React.FC = (props: Props) => { return ( <> + {!props.location?.endsWith('/mesh') && (

{t('More info at')} @@ -179,8 +181,10 @@ export const IstioStatusComponent: React.FC = (props: Props) => { dataTest: dataTest }; + const tooltipPosition = props.location === MASTHEAD ? TooltipPosition.bottom : TooltipPosition.top; + return ( - + {createIcon(iconProps, icon, iconColor)} ); diff --git a/plugin/src/kiali/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap b/plugin/src/kiali/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap index 665fee39..70478190 100644 --- a/plugin/src/kiali/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap +++ b/plugin/src/kiali/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap @@ -39,7 +39,7 @@ exports[`When addon component has a problem the Icon shows is displayed in orang position="top" > @@ -86,7 +86,7 @@ exports[`When both core and addon component have problems any component is in no position="top" > @@ -133,7 +133,7 @@ exports[`When core component has a problem the Icon shows is displayed in Red 1` position="top" > @@ -180,7 +180,7 @@ exports[`When there are not-ready components mixed with other not healthy compon position="top" > @@ -237,7 +237,7 @@ exports[`When there are not-ready components mixed with other not healthy compon position="top" > @@ -284,7 +284,7 @@ exports[`When there are not-ready components mixed with other not healthy compon position="top" > @@ -326,7 +326,7 @@ exports[`When there are not-ready components not mixed with other unhealthy comp position="top" > @@ -368,7 +368,7 @@ exports[`When there are not-ready components not mixed with other unhealthy comp position="top" > diff --git a/plugin/src/kiali/components/IstioWizards/ConfirmDeleteTrafficRoutingModal.tsx b/plugin/src/kiali/components/IstioWizards/ConfirmDeleteTrafficRoutingModal.tsx index 16737b36..37166c25 100644 --- a/plugin/src/kiali/components/IstioWizards/ConfirmDeleteTrafficRoutingModal.tsx +++ b/plugin/src/kiali/components/IstioWizards/ConfirmDeleteTrafficRoutingModal.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Button, ButtonVariant, Modal, ModalVariant } from '@patternfly/react-core'; import { DestinationRuleC, K8sGRPCRoute, K8sHTTPRoute, VirtualService } from '../../types/IstioObjects'; diff --git a/plugin/src/kiali/components/IstioWizards/Slider/BootstrapSlider.tsx b/plugin/src/kiali/components/IstioWizards/Slider/BootstrapSlider.tsx index c64cbd3a..f5e4bfc6 100644 --- a/plugin/src/kiali/components/IstioWizards/Slider/BootstrapSlider.tsx +++ b/plugin/src/kiali/components/IstioWizards/Slider/BootstrapSlider.tsx @@ -1,63 +1,67 @@ // Clone of Slider component to workaround issue https://github.com/patternfly/patternfly-react/issues/1221 -import React from 'react'; +import * as React from 'react'; import Slider from 'bootstrap-slider-without-jquery'; -import _ from 'lodash'; +import { min } from 'lodash-es'; const orientation = { horizontal: 'horizontal', vertical: 'vertical' }; -type Props = { - value: number; +type BootstrapSliderProps = { formatter: (value: any) => any; - onSlide: (event: any) => any; - onSlideStop: (event: any) => any; - orientation: string; - ticks: number[]; - ticks_labels: string[]; locked: boolean; - min: number; // Note that Slider will use max and maxLimit properties to: // maxLimit: the max value Slider can show // max: the max value Slider can enter // max < maxLimit when using Slider groups, so max can be relative max: number; maxLimit: number; + min: number; + onSlide: (event: any) => any; + onSlideStop: (event: any) => any; + orientation: string; + ticks: number[]; + ticks_labels: string[]; + value: number; }; -export class BootstrapSlider extends React.Component { +export class BootstrapSlider extends React.Component { static defaultProps = { - formatter: value => value, - onSlide: event => event, + formatter: (value: any): any => value, + onSlide: (event: any): any => event, orientation: 'horizontal', ticks: [], ticks_labels: [], locked: false }; + slider: Slider; sliderDiv: any; - componentDidMount() { + componentDidMount(): void { this.slider = new Slider(this.sliderDiv, { ...this.props }); - const onSlide = value => { + const onSlide = (value: any): void => { value = value >= this.props.max ? this.props.max : value; this.props.onSlide(value); this.slider.setValue(value); }; - const onSlideStop = value => { - value = _.min([value, this.props.max]); + + const onSlideStop = (value: any): void => { + value = min([value, this.props.max]); this.props.onSlideStop(value); this.slider.setValue(value); }; + this.slider.on('slide', onSlide); this.slider.on('slideStop', onSlideStop); this.slider.setAttribute('min', this.props.min); this.slider.setAttribute('max', this.props.maxLimit); + if (this.props.locked) { this.slider.disable(); } else { @@ -68,7 +72,7 @@ export class BootstrapSlider extends React.Component { // Instead of rendering the slider element again and again, // we took advantage of the bootstrap-slider library // and only update the new value or format when new props arrive. - componentDidUpdate(prevProps: Props) { + componentDidUpdate(prevProps: BootstrapSliderProps): void { if ( this.props.min !== prevProps.min || this.props.max !== prevProps.max || @@ -78,19 +82,23 @@ export class BootstrapSlider extends React.Component { this.slider.setAttribute('min', this.props.min); this.slider.setAttribute('max', this.props.maxLimit); this.slider.refresh(); - const onSlide = value => { + + const onSlide = (value: any): void => { value = value >= this.props.max ? this.props.max : value; this.props.onSlide(value); this.slider.setValue(value); }; - const onSlideStop = value => { - value = _.min([value, this.props.max]); + + const onSlideStop = (value: any): void => { + value = min([value, this.props.max]); this.props.onSlideStop(value); this.slider.setValue(value); }; + this.slider.on('slide', onSlide); this.slider.on('slideStop', onSlideStop); this.slider.setAttribute('formatter', this.props.formatter); + if (this.props.locked) { this.slider.disable(); } else { @@ -99,18 +107,19 @@ export class BootstrapSlider extends React.Component { } this.slider.setValue(this.props.value); + // Adjust the tooltip to "sit" ontop of the slider's handle. #LibraryBug if (this.props && this.props.orientation === orientation.horizontal) { this.slider.tooltip.style.marginLeft = `-${this.slider.tooltip.offsetWidth / 2}px`; if (this.props.ticks_labels && this.slider.tickLabelContainer) { - this.slider.tickLabelContainer.style.marginTop = '0px'; + this.slider.tickLabelContainer.style.marginTop = '0'; } } else { this.slider.tooltip.style.marginTop = `-${this.slider.tooltip.offsetHeight / 2}px`; } } - render() { + render(): React.ReactNode { return ( { @@ -21,14 +21,14 @@ export class Boundaries extends React.Component { showBoundaries: false }; - render() { + render(): React.ReactNode { const { children, min, max, reversed, showBoundaries, slider } = this.props; const minElement = {min}; const maxElement = {max}; - let leftBoundary: JSX.Element | null = null; - let rightBoundary: JSX.Element | null = null; + let leftBoundary: React.ReactNode = null; + let rightBoundary: React.ReactNode = null; if (showBoundaries) { if (reversed) { diff --git a/plugin/src/kiali/components/IstioWizards/Slider/Slider.tsx b/plugin/src/kiali/components/IstioWizards/Slider/Slider.tsx index e5faa640..5165e2ee 100644 --- a/plugin/src/kiali/components/IstioWizards/Slider/Slider.tsx +++ b/plugin/src/kiali/components/IstioWizards/Slider/Slider.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { BootstrapSlider } from './BootstrapSlider'; import { Button, ButtonVariant, InputGroupText, TextInput, Tooltip, TooltipPosition } from '@patternfly/react-core'; import { Boundaries } from './Boundaries'; @@ -7,37 +7,37 @@ import { MinusIcon, PlusIcon, ThumbTackIcon, MigrationIcon } from '@patternfly/r export const noop = Function.prototype; -type Props = { +type SliderProps = { id: string; - orientation: string; - min: number; + input: boolean; + inputFormat: string; + locked: boolean; max: number; maxLimit: number; + min: number; + mirrored: boolean; + onLock: (locked: boolean) => void; + onMirror: (mirror: boolean) => void; + onSlide: (value: number) => void; + onSlideStop: (value: number) => void; + orientation: string; + showLock: boolean; + showMirror: boolean; + sliderClass: string; step: number; - value: number; ticks: number[]; ticks_labels: string[]; tooltip: boolean; tooltipFormatter: (value: number) => string; - onSlide: (value: number) => void; - onSlideStop: (value: number) => void; - input: boolean; - sliderClass: string; - inputFormat: string; - locked: boolean; - showLock: boolean; - onLock: (locked: boolean) => void; - mirrored: boolean; - showMirror: boolean; - onMirror: (mirror: boolean) => void; + value: number; }; -type State = { - value: number; +type SliderState = { tooltipFormat: string; + value: number; }; -export class Slider extends React.Component { +export class Slider extends React.Component { static defaultProps = { id: null, orientation: 'horizontal', @@ -64,7 +64,7 @@ export class Slider extends React.Component { onMirror: noop }; - constructor(props: Props) { + constructor(props: SliderProps) { super(props); this.state = { @@ -73,61 +73,63 @@ export class Slider extends React.Component { }; } - componentDidMount() { + componentDidMount(): void { // This empty setState forces a re-render which resolves an issue with initial tick_label placement this.setState({}); } - componentDidUpdate(prevProps: Readonly): void { + componentDidUpdate(prevProps: Readonly): void { if (prevProps.value !== this.props.value || this.state.value !== this.props.value) { this.setState({ value: this.props.value }); } } - onSlide = value => { + onSlide = (value: number): void => { this.setState({ value }, () => this.props.onSlide(value)); }; - onSlideStop = value => { + onSlideStop = (value: number): void => { this.setState({ value }, () => this.props.onSlideStop(value)); }; - onPlus = () => { + onPlus = (): void => { const newValue = Number(this.state.value || 0); this.updateNewValue(newValue + 1); }; - onMinus = () => { + onMinus = (): void => { const newValue = Number(this.state.value || 0); this.updateNewValue(newValue - 1); }; - onInputChange = (value: string | number) => { - const newValue: number = Number(value); + onInputChange = (value: string | number): void => { + const newValue = Number(value); this.updateNewValue(Number.isNaN(newValue) ? 0 : newValue); }; - updateNewValue = (newValue: number) => { + updateNewValue = (newValue: number): void => { if (newValue > this.props.max) { newValue = this.props.max; } + if (newValue < 0) { newValue = 0; } + this.setState({ value: newValue }, () => this.props.onSlide(newValue)); }; - onFormatChange = format => { + onFormatChange = (format: string): void => { this.setState({ tooltipFormat: format }); }; - formatter = value => { + formatter = (value: number): string => { return this.props.tooltipFormatter !== noop ? this.props.tooltipFormatter(value) : `${value} ${this.state.tooltipFormat} ${this.props.mirrored ? ' mirrored traffic' : ''}`; }; - render() { + render(): React.ReactNode { const BSSlider = ( { value={this.state.value} onChange={(_event, value: string | number) => this.onInputChange(value)} isDisabled={this.props.locked} - data-test={'input-' + this.props.id} + data-test={`input-${this.props.id}`} /> ); }; - private renderMessageCenterBadge = () => { - const bell = kialiStyle({ - position: 'relative', - right: '5px', - top: '2px' - }); - const count = kialiStyle({ - position: 'relative', - top: '2px', - verticalAlign: '0.125em' - }); + const renderMessageCenterBadge = (): React.ReactNode => { + let notificationVariant = NotificationBadgeVariant.read; + + if (props.newMessagesCount > 0) { + if (props.badgeDanger) { + notificationVariant = NotificationBadgeVariant.attention; + } else { + notificationVariant = NotificationBadgeVariant.unread; + } + } return ( - + ); }; -} -const mapStateToPropsMessageCenterTrigger = (state: KialiAppState) => { + return ( + <> + {renderSystemErrorBadge()} + {renderMessageCenterBadge()} + + ); +}; + +const mapStateToPropsMessageCenterTrigger = (state: KialiAppState): ReduxStateProps => { type MessageCenterTriggerPropsToMap = { - newMessagesCount: number; badgeDanger: boolean; + newMessagesCount: number; systemErrorsCount: number; }; @@ -116,7 +124,7 @@ const mapStateToPropsMessageCenterTrigger = (state: KialiAppState) => { ); }; -const mapDispatchToPropsMessageCenterTrigger = (dispatch: KialiDispatch) => { +const mapDispatchToPropsMessageCenterTrigger = (dispatch: KialiDispatch): ReduxDispatchProps => { return { toggleMessageCenter: () => dispatch(MessageCenterThunkActions.toggleMessageCenter()), toggleSystemErrorsCenter: () => dispatch(MessageCenterThunkActions.toggleSystemErrorsCenter()) diff --git a/plugin/src/kiali/components/MessageCenter/NotificationList.tsx b/plugin/src/kiali/components/MessageCenter/NotificationList.tsx index 8c52cc9a..cfa3a7a8 100644 --- a/plugin/src/kiali/components/MessageCenter/NotificationList.tsx +++ b/plugin/src/kiali/components/MessageCenter/NotificationList.tsx @@ -1,50 +1,42 @@ import * as React from 'react'; import { NotificationMessage, MessageType } from '../../types/MessageCenter'; -import { AlertVariant } from '@patternfly/react-core'; -import { AlertToast } from './AlertToast'; +import { Alert, AlertActionCloseButton, AlertGroup, AlertVariant } from '@patternfly/react-core'; type NotificationListProps = { messages: NotificationMessage[]; - onDismiss?: (message: NotificationMessage, userDismissed: boolean) => void; + onDismiss: (message: NotificationMessage, userDismissed: boolean) => void; }; -export class NotificationList extends React.PureComponent { - render() { - return ( - <> - {this.props.messages.map((message, i) => { - let variant: AlertVariant; - switch (message.type) { - case MessageType.SUCCESS: - variant = AlertVariant.success; - break; - case MessageType.WARNING: - variant = AlertVariant.warning; - break; - case MessageType.INFO: - variant = AlertVariant.info; - break; - default: - variant = AlertVariant.danger; - } - const onClose = this.props.onDismiss - ? () => { - this.props.onDismiss!(message, true); - } - : undefined; - return ( - - ); - })} - - ); - } -} +export const NotificationList: React.FC = (props: NotificationListProps) => { + return ( + + {props.messages.map(message => { + let variant: AlertVariant; + + switch (message.type) { + case MessageType.SUCCESS: + variant = AlertVariant.success; + break; + case MessageType.WARNING: + variant = AlertVariant.warning; + break; + case MessageType.INFO: + variant = AlertVariant.info; + break; + default: + variant = AlertVariant.danger; + } + + return ( + props.onDismiss(message, true)} />} + /> + ); + })} + + ); +}; diff --git a/plugin/src/kiali/components/Metrics/CustomMetrics.tsx b/plugin/src/kiali/components/Metrics/CustomMetrics.tsx index 6d0d417a..9cd72664 100644 --- a/plugin/src/kiali/components/Metrics/CustomMetrics.tsx +++ b/plugin/src/kiali/components/Metrics/CustomMetrics.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import { Toolbar, ToolbarGroup, @@ -14,7 +13,7 @@ import { } from '@patternfly/react-core'; import { kialiStyle } from 'styles/StyleUtils'; import { serverConfig } from '../../config/ServerConfig'; -import { history, HistoryManager, URLParam } from '../../app/History'; +import { router, HistoryManager, URLParam, location } from '../../app/History'; import * as API from '../../services/Api'; import { KialiAppState } from '../../store/Store'; import { TimeRange, evalTimeRange, TimeInMilliseconds, isEqualTimeRange } from '../../types/Common'; @@ -52,7 +51,7 @@ type MetricsState = { tabHeight: number; }; -type CustomMetricsProps = RouteComponentProps<{}> & { +type CustomMetricsProps = { app: string; embedded?: boolean; height?: number; @@ -216,7 +215,7 @@ class CustomMetricsComponent extends React.Component { if (isParentKiosk(this.props.kiosk)) { kioskContextMenuAction(traceUrl); } else { - history.push(traceUrl); + router.navigate(traceUrl); } } }; @@ -243,7 +242,7 @@ class CustomMetricsComponent extends React.Component { }; render(): React.ReactNode { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); const expandedChart = urlParams.get('expand') || undefined; // 20px (card margin) + 24px (card padding) + 51px (toolbar) + 15px (toolbar padding) + 24px (card padding) + 20px (card margin) @@ -298,10 +297,10 @@ class CustomMetricsComponent extends React.Component { } private onSpans = (checked: boolean): void => { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); urlParams.set(URLParam.SHOW_SPANS, String(checked)); - history.replace(`${history.location.pathname}?${urlParams.toString()}`); + router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true }); this.setState({ showSpans: !this.state.showSpans }); }; @@ -365,14 +364,14 @@ class CustomMetricsComponent extends React.Component { }; private expandHandler = (expandedChart?: string): void => { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); urlParams.delete('expand'); if (expandedChart) { urlParams.set('expand', expandedChart); } - history.push(`${history.location.pathname}?${urlParams.toString()}`); + router.navigate(`${location.getPathname()}?${urlParams.toString()}`); }; private toggleTimeOptionsVisibility = (): void => { @@ -394,6 +393,4 @@ const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => { }; }; -export const CustomMetrics = withRouter & CustomMetricsProps, any>( - connect(mapStateToProps, mapDispatchToProps)(CustomMetricsComponent) -); +export const CustomMetrics = connect(mapStateToProps, mapDispatchToProps)(CustomMetricsComponent); diff --git a/plugin/src/kiali/components/Metrics/Helper.ts b/plugin/src/kiali/components/Metrics/Helper.ts index 9c540e2b..cb9fd4c4 100644 --- a/plugin/src/kiali/components/Metrics/Helper.ts +++ b/plugin/src/kiali/components/Metrics/Helper.ts @@ -1,11 +1,12 @@ import { MetricsSettings, LabelsSettings, Quantiles, LabelSettings } from '../MetricsOptions/MetricsSettings'; import { boundsToDuration, guardTimeRange, TimeRange, DurationInSeconds } from '../../types/Common'; import { computePrometheusRateParams } from '../../services/Prometheus'; -import { history, URLParam } from '../../app/History'; +import { URLParam, location } from '../../app/History'; import { responseFlags } from 'utils/ResponseFlags'; import { AggregationModel, DashboardModel } from 'types/Dashboards'; import { AllPromLabelsValues, Metric, PromLabel, SingleLabelValues } from 'types/Metrics'; import { MetricsQuery } from 'types/MetricsOptions'; + // Default to 10 minutes. Showing timeseries to only 1 minute doesn't make so much sense. export const defaultMetricsDuration: DurationInSeconds = 600; @@ -15,20 +16,26 @@ export const combineLabelsSettings = (newSettings: LabelsSettings, stateSettings // so we can override them in props from state // LabelsSettings received from props contains the names of the filters with only a default on/off flag. const result: LabelsSettings = new Map(); + newSettings.forEach((lblObj, promLabel) => { const resultValues: SingleLabelValues = {}; const stateObj = stateSettings.get(promLabel); + Object.entries(lblObj.values).forEach(e => { resultValues[e[0]] = stateObj && stateObj.defaultValue === false ? false : e[1]; }); + if (stateObj) { lblObj.checked = stateObj.checked; + Object.entries(stateObj.values).forEach(e => { resultValues[e[0]] = e[1]; }); } + result.set(promLabel, { ...lblObj, values: resultValues }); }); + return result; }; @@ -40,9 +47,11 @@ export const extractLabelsSettingsOnSeries = ( metrics.forEach(m => { Object.keys(m.labels).forEach(k => { const agg = aggregations.find(a => a.label === k); + if (agg) { const value = m.labels[k]; let lblObj = extracted.get(agg.label); + if (!lblObj) { lblObj = { checked: true, @@ -51,10 +60,12 @@ export const extractLabelsSettingsOnSeries = ( defaultValue: true, singleSelection: agg.singleSelection }; + extracted.set(agg.label, lblObj); } else { lblObj.checked = true; } + if (!lblObj.values.hasOwnProperty(value)) { if (agg.singleSelection && Object.keys(lblObj.values).length > 0) { // In single-selection mode, do not activate more than one label value at a time @@ -71,6 +82,7 @@ export const extractLabelsSettingsOnSeries = ( export const extractLabelsSettings = (dashboard: DashboardModel, stateSettings: LabelsSettings): LabelsSettings => { // Find all labels on all series const newSettings: LabelsSettings = new Map(); + dashboard.aggregations.forEach(agg => newSettings.set(agg.label, { checked: false, @@ -80,6 +92,7 @@ export const extractLabelsSettings = (dashboard: DashboardModel, stateSettings: singleSelection: agg.singleSelection }) ); + dashboard.charts.forEach(chart => extractLabelsSettingsOnSeries(chart.metrics, dashboard.aggregations, newSettings)); return combineLabelsSettings(newSettings, stateSettings); }; @@ -94,29 +107,35 @@ export const mergeLabelFilter = ( // Note: we don't really care that the new map references same objects as the old one (at least at the moment) so shallow copy is fine const newSettings = new Map(lblSettings); const objLbl = newSettings.get(label); + if (objLbl) { if (singleSelection) { for (const v of Object.keys(objLbl.values)) { objLbl.values[v] = false; } } + objLbl.values[value] = checked; } + return newSettings; }; export const convertAsPromLabels = (lblSettings: LabelsSettings): AllPromLabelsValues => { const promLabels = new Map(); + lblSettings.forEach((objLbl, k) => { promLabels.set(k, objLbl.values); }); + return promLabels; }; -export const settingsToOptions = (settings: MetricsSettings, opts: MetricsQuery, defaultLabels: string[]) => { +export const settingsToOptions = (settings: MetricsSettings, opts: MetricsQuery, defaultLabels: string[]): void => { opts.avg = settings.showAverage; opts.quantiles = settings.showQuantiles; let byLabels = defaultLabels; + if (settings.labelsSettings.size > 0) { // Labels have been fetched, so use what comes from labelsSettings byLabels = []; @@ -126,11 +145,13 @@ export const settingsToOptions = (settings: MetricsSettings, opts: MetricsQuery, } }); } + opts.byLabels = byLabels; }; -export const timeRangeToOptions = (range: TimeRange, opts: MetricsQuery) => { +export const timeRangeToOptions = (range: TimeRange, opts: MetricsQuery): void => { delete opts.queryTime; + opts.duration = guardTimeRange( range, d => d, @@ -139,13 +160,15 @@ export const timeRangeToOptions = (range: TimeRange, opts: MetricsQuery) => { return boundsToDuration(ft); } ); + const intervalOpts = computePrometheusRateParams(opts.duration); opts.step = intervalOpts.step; opts.rateInterval = intervalOpts.rateInterval; }; export const retrieveMetricsSettings = (): MetricsSettings => { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); + const settings: MetricsSettings = { showSpans: false, showTrendlines: false, @@ -153,18 +176,22 @@ export const retrieveMetricsSettings = (): MetricsSettings => { showQuantiles: [], labelsSettings: new Map() }; + const avg = urlParams.get(URLParam.SHOW_AVERAGE); if (avg !== null) { settings.showAverage = avg === 'true'; } + const spans = urlParams.get(URLParam.SHOW_SPANS); if (spans !== null) { settings.showSpans = spans === 'true'; } + const trendlines = urlParams.get(URLParam.SHOW_TRENDLINES); if (trendlines !== null) { settings.showTrendlines = trendlines === 'true'; } + const quantiles = urlParams.get(URLParam.QUANTILES); if (quantiles !== null) { if (quantiles.trim().length !== 0) { @@ -173,11 +200,13 @@ export const retrieveMetricsSettings = (): MetricsSettings => { settings.showQuantiles = []; } } + const byLabels = urlParams.getAll(URLParam.BY_LABELS); // E.g.: bylbl=version=v1,v2,v4 if (byLabels.length !== 0) { byLabels.forEach(val => { const kvpair = val.split('=', 2); + const lblObj: LabelSettings = { displayName: '', checked: true, @@ -185,6 +214,7 @@ export const retrieveMetricsSettings = (): MetricsSettings => { defaultValue: true, singleSelection: false }; + if (kvpair[1]) { kvpair[1].split(',').forEach(v => { lblObj.values[v] = true; @@ -192,9 +222,11 @@ export const retrieveMetricsSettings = (): MetricsSettings => { // When values filters are provided by URL, other filters should be false by default lblObj.defaultValue = false; } + settings.labelsSettings.set(kvpair[0], lblObj); }); } + return settings; }; @@ -203,11 +235,14 @@ export const prettyLabelValues = (promName: PromLabel, val: string): string => { if (val === '-') { return 'None'; } + const flagObj = responseFlags[val]; + if (flagObj) { const text = flagObj.short ? flagObj.short : flagObj.help; return `${text} (${val})`; } } + return val; }; diff --git a/plugin/src/kiali/components/Metrics/IstioMetrics.tsx b/plugin/src/kiali/components/Metrics/IstioMetrics.tsx index 088caa4e..608e22f6 100644 --- a/plugin/src/kiali/components/Metrics/IstioMetrics.tsx +++ b/plugin/src/kiali/components/Metrics/IstioMetrics.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { KialiDispatch } from 'types/Redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import { Card, CardBody, Checkbox, Toolbar, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; import { kialiStyle } from 'styles/StyleUtils'; import * as API from 'services/Api'; @@ -17,7 +16,7 @@ import { MetricsSettings, LabelsSettings } from '../MetricsOptions/MetricsSettin import { MetricsSettingsDropdown } from '../MetricsOptions/MetricsSettingsDropdown'; import { MetricsReporter } from '../MetricsOptions/MetricsReporter'; import { TimeDurationModal } from '../Time/TimeDurationModal'; -import { history, URLParam } from 'app/History'; +import { location, router, URLParam } from 'app/History'; import { MetricsObjectTypes } from 'types/Metrics'; import { GrafanaInfo } from 'types/GrafanaInfo'; import { MessageType } from 'types/MessageCenter'; @@ -52,14 +51,13 @@ type ObjectId = { object: string; }; -type IstioMetricsProps = ObjectId & - RouteComponentProps<{}> & { - direction: Direction; - includeAmbient: boolean; - objectType: MetricsObjectTypes; - } & { - lastRefreshAt: TimeInMilliseconds; - }; +type IstioMetricsProps = ObjectId & { + direction: Direction; + includeAmbient: boolean; + objectType: MetricsObjectTypes; +} & { + lastRefreshAt: TimeInMilliseconds; +}; type ReduxStateProps = { kiosk: string; @@ -213,6 +211,7 @@ class IstioMetricsComponent extends React.Component { if (response.status === 204) { return undefined; } + return response.data; }); } @@ -273,7 +272,7 @@ class IstioMetricsComponent extends React.Component { if (isParentKiosk(this.props.kiosk)) { kioskContextMenuAction(traceUrl); } else { - history.push(traceUrl); + router.navigate(traceUrl); } } }; @@ -290,7 +289,7 @@ class IstioMetricsComponent extends React.Component { } render(): React.ReactNode { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); const expandedChart = urlParams.get('expand') ?? undefined; // 20px (card margin) + 24px (card padding) + 51px (toolbar) + 15px (toolbar padding) + 24px (card padding) + 20px (card margin) @@ -336,16 +335,16 @@ class IstioMetricsComponent extends React.Component { } private onSpans = (checked: boolean): void => { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); urlParams.set(URLParam.SHOW_SPANS, String(checked)); - history.replace(`${history.location.pathname}?${urlParams.toString()}`); + router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true }); this.setState({ showSpans: !this.state.showSpans }); }; private onTrendlines = (checked: boolean): void => { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); urlParams.set(URLParam.SHOW_TRENDLINES, String(checked)); - history.replace(`${history.location.pathname}?${urlParams.toString()}`); + router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true }); this.setState({ showTrendlines: !this.state.showTrendlines }); }; @@ -429,14 +428,14 @@ class IstioMetricsComponent extends React.Component { } private expandHandler = (expandedChart?: string): void => { - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); urlParams.delete('expand'); if (expandedChart) { urlParams.set('expand', expandedChart); } - history.push(`${history.location.pathname}?${urlParams.toString()}`); + router.navigate(`${location.getPathname()}?${urlParams.toString()}`); }; } @@ -455,6 +454,4 @@ const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => { }; }; -export const IstioMetrics = withRouter & IstioMetricsProps, any>( - connect(mapStateToProps, mapDispatchToProps)(IstioMetricsComponent) -); +export const IstioMetrics = connect(mapStateToProps, mapDispatchToProps)(IstioMetricsComponent); diff --git a/plugin/src/kiali/components/Metrics/__tests__/IstioMetrics.test.tsx b/plugin/src/kiali/components/Metrics/__tests__/IstioMetrics.test.tsx index 5c0925d3..ad37e73b 100644 --- a/plugin/src/kiali/components/Metrics/__tests__/IstioMetrics.test.tsx +++ b/plugin/src/kiali/components/Metrics/__tests__/IstioMetrics.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import { Provider } from 'react-redux'; -import { MemoryRouter, Route } from 'react-router'; +import { MemoryRouter } from 'react-router-dom-v5-compat'; import { shallowToJson } from 'enzyme-to-json'; import { IstioMetrics } from '../IstioMetrics'; import * as API from '../../../services/Api'; @@ -101,18 +101,13 @@ describe('Metrics for a service', () => { const wrapper = shallow( - ( - - )} + @@ -131,7 +126,7 @@ describe('Metrics for a service', () => { objectType={MetricsObjectTypes.SERVICE} direction={'inbound'} includeAmbient={false} - lastRefreshAt={Date.now()} + lastRefreshAt={1720526431902} /> ) .run(done, wrapper => { @@ -161,7 +156,7 @@ describe('Metrics for a service', () => { objectType={MetricsObjectTypes.SERVICE} direction={'inbound'} includeAmbient={false} - lastRefreshAt={Date.now()} + lastRefreshAt={1720526431902} /> ) .run(done, wrapper => { @@ -192,19 +187,16 @@ describe('Inbound Metrics for a workload', () => { it('renders initial layout', () => { const wrapper = shallow( - ( - - )} - /> + + + ); expect(shallowToJson(wrapper)).toMatchSnapshot(); @@ -221,7 +213,7 @@ describe('Inbound Metrics for a workload', () => { objectType={MetricsObjectTypes.WORKLOAD} direction={'inbound'} includeAmbient={false} - lastRefreshAt={Date.now()} + lastRefreshAt={1720526431902} /> ) .run(done, wrapper => { @@ -251,7 +243,7 @@ describe('Inbound Metrics for a workload', () => { objectType={MetricsObjectTypes.WORKLOAD} direction={'inbound'} includeAmbient={false} - lastRefreshAt={Date.now()} + lastRefreshAt={1720526431902} /> ) .run(done, wrapper => { diff --git a/plugin/src/kiali/components/Metrics/__tests__/__snapshots__/IstioMetrics.test.tsx.snap b/plugin/src/kiali/components/Metrics/__tests__/__snapshots__/IstioMetrics.test.tsx.snap index ed51a760..30d7c89f 100644 --- a/plugin/src/kiali/components/Metrics/__tests__/__snapshots__/IstioMetrics.test.tsx.snap +++ b/plugin/src/kiali/components/Metrics/__tests__/__snapshots__/IstioMetrics.test.tsx.snap @@ -30,9 +30,16 @@ exports[`Inbound Metrics for a workload renders initial layout 1`] = ` } } > - + + + `; @@ -67,8 +74,13 @@ exports[`Metrics for a service renders initial layout 1`] = ` } > - diff --git a/plugin/src/kiali/components/MetricsOptions/MetricsSettingsDropdown.tsx b/plugin/src/kiali/components/MetricsOptions/MetricsSettingsDropdown.tsx index 6080f835..6401fdce 100644 --- a/plugin/src/kiali/components/MetricsOptions/MetricsSettingsDropdown.tsx +++ b/plugin/src/kiali/components/MetricsOptions/MetricsSettingsDropdown.tsx @@ -12,7 +12,7 @@ import { } from '@patternfly/react-core'; import { kialiStyle } from 'styles/StyleUtils'; import isEqual from 'lodash/isEqual'; -import { history, URLParam } from '../../app/History'; +import { location, router, URLParam } from '../../app/History'; import { MetricsSettings, Quantiles, allQuantiles, LabelsSettings } from './MetricsSettings'; import { mergeLabelFilter, @@ -55,8 +55,9 @@ export class MetricsSettingsDropdown extends React.Component { this.state = { ...settings, isOpen: false, allSelected: false }; } - checkSelected = () => { + checkSelected = (): void => { let allSelected = true; + this.state.labelsSettings.forEach(lblSetting => { if (lblSetting.checked === false) { allSelected = false; @@ -72,7 +73,7 @@ export class MetricsSettingsDropdown extends React.Component { this.setState({ allSelected: allSelected }); }; - componentDidUpdate(prevProps: Props) { + componentDidUpdate(prevProps: Props): void { // TODO Move the sync of URL and state to a global place const changeDirection = prevProps.direction !== this.props.direction; const settings = retrieveMetricsSettings(); @@ -92,11 +93,11 @@ export class MetricsSettingsDropdown extends React.Component { } } - private onToggle = isOpen => { + private onToggle = (isOpen: boolean): void => { this.setState({ isOpen: isOpen }); }; - onGroupingChanged = (label: PromLabel, checked: boolean) => { + onGroupingChanged = (label: PromLabel, checked: boolean): void => { const objLbl = this.state.labelsSettings.get(label); if (objLbl) { @@ -116,7 +117,7 @@ export class MetricsSettingsDropdown extends React.Component { ); }; - onLabelsFiltersChanged = (label: PromLabel, value: string, checked: boolean, singleSelection: boolean) => { + onLabelsFiltersChanged = (label: PromLabel, value: string, checked: boolean, singleSelection: boolean): void => { const newValues = mergeLabelFilter(this.state.labelsSettings, label, value, checked, singleSelection); this.updateLabelsSettingsURL(newValues); @@ -126,9 +127,9 @@ export class MetricsSettingsDropdown extends React.Component { }); }; - updateLabelsSettingsURL = (labelsSettings: LabelsSettings) => { + updateLabelsSettingsURL = (labelsSettings: LabelsSettings): void => { // E.g.: bylbl=version=v1,v2,v4 - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); urlParams.delete(URLParam.BY_LABELS); labelsSettings.forEach((lbl, name) => { @@ -136,33 +137,34 @@ export class MetricsSettingsDropdown extends React.Component { const filters = Object.keys(lbl.values) .filter(k => lbl.values[k]) .join(','); + if (filters) { - urlParams.append(URLParam.BY_LABELS, name + '=' + filters); + urlParams.append(URLParam.BY_LABELS, `${name}=${filters}`); } else { urlParams.append(URLParam.BY_LABELS, name); } } }); - history.replace(history.location.pathname + '?' + urlParams.toString()); + router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true }); }; - onHistogramAverageChanged = (checked: boolean) => { - const urlParams = new URLSearchParams(history.location.search); + onHistogramAverageChanged = (checked: boolean): void => { + const urlParams = new URLSearchParams(location.getSearch()); urlParams.set(URLParam.SHOW_AVERAGE, String(checked)); - history.replace(history.location.pathname + '?' + urlParams.toString()); + router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true }); this.setState({ showAverage: checked }, () => this.props.onChanged(this.state)); }; - onHistogramOptionsChanged = (quantile: Quantiles, checked: boolean) => { + onHistogramOptionsChanged = (quantile: Quantiles, checked: boolean): void => { const newQuantiles = checked ? [quantile].concat(this.state.showQuantiles) : this.state.showQuantiles.filter(q => quantile !== q); - const urlParams = new URLSearchParams(history.location.search); + const urlParams = new URLSearchParams(location.getSearch()); urlParams.set(URLParam.QUANTILES, newQuantiles.join(' ')); - history.replace(history.location.pathname + '?' + urlParams.toString()); + router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true }); this.setState({ showQuantiles: newQuantiles }, () => this.props.onChanged(this.state)); }; @@ -188,17 +190,17 @@ export class MetricsSettingsDropdown extends React.Component { ); }; - onBulkAll = () => { + onBulkAll = (): void => { this.bulkUpdate(true); this.setState({ allSelected: true }); }; - onBulkNone = () => { + onBulkNone = (): void => { this.bulkUpdate(false); this.setState({ allSelected: false }); }; - render() { + render(): React.ReactNode { const hasHistograms = this.props.hasHistograms; const hasLabels = this.state.labelsSettings.size > 0; @@ -225,7 +227,7 @@ export class MetricsSettingsDropdown extends React.Component { ); } - renderBulkSelector(): JSX.Element { + renderBulkSelector(): React.ReactNode { return (
@@ -249,14 +251,14 @@ export class MetricsSettingsDropdown extends React.Component { ); } - renderLabelOptions(): JSX.Element { + renderLabelOptions(): React.ReactNode { const displayGroupingLabels: any[] = []; this.state.labelsSettings.forEach((lblObj, promName) => { const labelsHTML = lblObj.checked && lblObj.values ? Object.keys(lblObj.values).map(val => ( -
+
{lblObj.singleSelection ? ( { : null; displayGroupingLabels.push( -
+