diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 06096b1c3de5b..a3d9e8884d7d3 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -42,7 +42,13 @@ import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_a import { AgentBasedPackagePoliciesTable } from './components/agent_based_table'; import { AgentlessPackagePoliciesTable } from './components/agentless_table'; -export const PackagePoliciesPage = ({ packageInfo }: { packageInfo: PackageInfo }) => { +export const PackagePoliciesPage = ({ + packageInfo, + embedded, +}: { + packageInfo: PackageInfo; + embedded?: boolean; +}) => { const { name, version } = packageInfo; const { search } = useLocation(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); @@ -154,7 +160,11 @@ export const PackagePoliciesPage = ({ packageInfo }: { packageInfo: PackageInfo // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab // Check `addAgentToPolicyIdFromParams` otherwise right after installing a new integration the flyout won't open - if (packageInstallStatus.status !== InstallStatus.installed && !addAgentToPolicyIdFromParams) { + if ( + packageInstallStatus && + packageInstallStatus.status !== InstallStatus.installed && + !addAgentToPolicyIdFromParams + ) { return ( ); @@ -170,7 +180,7 @@ export const PackagePoliciesPage = ({ packageInfo }: { packageInfo: PackageInfo }} > - + {embedded ? null : } {!canHaveAgentlessPolicies ? ( -

+

-

+
-

{agentlessData?.total ?? 0}

+

{agentlessData?.total ?? 0}

@@ -244,17 +254,17 @@ export const PackagePoliciesPage = ({ packageInfo }: { packageInfo: PackageInfo > -

+

-

+
-

{agentBasedData?.total ?? 0}

+

{agentBasedData?.total ?? 0}

diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/components/installed_integrations_table.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/components/installed_integrations_table.tsx index 741c6bd275c2e..ad4e000f06e53 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/components/installed_integrations_table.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/components/installed_integrations_table.tsx @@ -22,6 +22,7 @@ import { TableIcon } from '../../../../../../../components/package_icon'; import type { PackageListItem } from '../../../../../../../../common'; import { type UrlPagination, useLink, useAuthz } from '../../../../../../../hooks'; import type { InstalledPackageUIPackageListItem } from '../types'; +import { useViewPolicies } from '../hooks/use_url_filters'; import { InstallationVersionStatus } from './installation_version_status'; import { DisabledWrapperTooltip } from './disabled_wrapper_tooltip'; @@ -39,6 +40,7 @@ export const InstalledIntegrationsTable: React.FunctionComponent<{ const authz = useAuthz(); const { getHref } = useLink(); const { selectedItems, setSelectedItems } = selection; + const { addViewPolicies } = useViewPolicies(); const { setPagination } = pagination; const handleTablePagination = React.useCallback( @@ -148,7 +150,7 @@ export const InstalledIntegrationsTable: React.FunctionComponent<{ } disabled={isDisabled} > - {}} disabled={isDisabled}> + addViewPolicies(item.name)} disabled={isDisabled}> = ({ installedPackage }) => { + const { addViewPolicies } = useViewPolicies(); + const paddingStyles = useEuiPaddingCSS(); + const cssStyles = [paddingStyles.s]; + + const title = ( + + + + + + {installedPackage.title} + + + ); + + const content = ( +
+ + + +
+ ); + + return addViewPolicies('')} />; +}; diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/components/resizable_panel.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/components/resizable_panel.tsx new file mode 100644 index 0000000000000..a57256f8f713f --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/components/resizable_panel.tsx @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPortal, + useEuiPaddingCSS, + useEuiTheme, +} from '@elastic/eui'; +import { EuiResizableButton, EuiPanel, keys } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; + +const getMouseOrTouchY = ( + e: TouchEvent | MouseEvent | React.MouseEvent | React.TouchEvent +): number => { + const x = (e as TouchEvent).targetTouches + ? (e as TouchEvent).targetTouches[0].pageY + : (e as MouseEvent).pageY; + return -x; +}; + +export const ResizablePanelComponent: React.FunctionComponent<{ + topBar: React.ReactNode; + children: React.ReactNode; + isCollapsed: boolean; +}> = ({ children, isCollapsed, topBar }) => { + const euiTheme = useEuiTheme(); + const [panelHeight, setPanelHeight] = useState(300); + const initialPanelHeight = useRef(panelHeight); + const initialMouseY = useRef(0); + + const normalizeHeight = useCallback( + (height: number) => { + const marginTop = parseInt(euiTheme.euiTheme.size.xxxxl, 10); + // Be sure to not go over top bar + return Math.min(Math.max(height, 0), window.innerHeight - marginTop * 3); + }, + [euiTheme.euiTheme.size.xxxxl] + ); + + useEffect(() => { + function onResize() { + const normalizedHeight = normalizeHeight(panelHeight); + if (normalizedHeight !== panelHeight) { + setPanelHeight(normalizedHeight); + } + } + window.addEventListener('resize', onResize); + return () => { + window.removeEventListener('resize', onResize); + }; + }, [panelHeight, normalizeHeight]); + + const onMouseMove = useCallback( + (e: MouseEvent | TouchEvent) => { + const mouseOffset = getMouseOrTouchY(e) - initialMouseY.current; + const changedPanelHeight = initialPanelHeight.current + mouseOffset; + + setPanelHeight(normalizeHeight(changedPanelHeight)); + }, + [normalizeHeight] + ); + + const onMouseUp = useCallback(() => { + initialMouseY.current = 0; + + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + window.removeEventListener('touchmove', onMouseMove); + window.removeEventListener('touchend', onMouseUp); + }, [onMouseMove]); + + const onMouseDown = useCallback( + (e: React.MouseEvent | React.TouchEvent) => { + initialMouseY.current = getMouseOrTouchY(e); + initialPanelHeight.current = panelHeight; + + // Window event listeners instead of React events are used + // in case the user's mouse leaves the component + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + window.addEventListener('touchmove', onMouseMove); + window.addEventListener('touchend', onMouseUp); + }, + [panelHeight, onMouseMove, onMouseUp] + ); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + const KEYBOARD_OFFSET = 10; + + switch (e.key) { + case keys.ARROW_UP: + e.preventDefault(); // Safari+VO will screen reader navigate off the button otherwise + setPanelHeight((currentPanelHeight) => + normalizeHeight(currentPanelHeight + KEYBOARD_OFFSET) + ); + break; + case keys.ARROW_DOWN: + e.preventDefault(); // Safari+VO will screen reader navigate off the button otherwise + setPanelHeight((currentPanelHeight) => + normalizeHeight(currentPanelHeight - KEYBOARD_OFFSET) + ); + } + }, + [normalizeHeight] + ); + + return ( + + {topBar} + + + {children} + + + ); +}; + +export const ResizablePanel: React.FunctionComponent<{ + title: React.ReactNode; + content: React.ReactNode; + onClose: () => void; +}> = ({ title, content, onClose }) => { + const euiTheme = useEuiTheme(); + + const paddingStyles = useEuiPaddingCSS(); + const cssStyles = [paddingStyles.m]; + + const toggleCollpase = useCallback(() => { + setIsCollapsed((current) => !current); + }, []); + + const [isCollapsed, setIsCollapsed] = useState(false); + + const topBar = ( + + {title} + + + + {isCollapsed ? ( + + ) : ( + + )} + + + + + + + + ); + + return ( + + + {content} + + + ); +}; diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/hooks/use_url_filters.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/hooks/use_url_filters.tsx index 58087c5a2ca37..86b4ecac428ff 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/hooks/use_url_filters.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/hooks/use_url_filters.tsx @@ -46,6 +46,39 @@ export function useAddUrlFilters() { ); } +export function useViewPolicies() { + const { toUrlParams, urlParams } = useUrlParams(); + const history = useHistory(); + + const addViewPolicies = useCallback( + (packageName: string) => { + history.push({ + search: toUrlParams( + { + ...omit(urlParams, 'viewPolicies'), + ...(packageName ? { viewPolicies: packageName } : {}), + }, + { + skipEmptyString: true, + } + ), + }); + }, + [urlParams, toUrlParams, history] + ); + + const selectedPackageViewPolicies = useMemo(() => { + if (typeof urlParams.viewPolicies === 'string') { + return urlParams.viewPolicies; + } + }, [urlParams]); + + return { + addViewPolicies, + selectedPackageViewPolicies, + }; +} + export function useUrlFilters(): InstalledIntegrationsFilter { const { urlParams } = useUrlParams(); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/index.tsx index 7d48d0ec10222..0c86b75e72e2d 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/installed_integrations/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { EuiSpacer } from '@elastic/eui'; import styled from '@emotion/styled'; @@ -14,10 +14,11 @@ import { useUrlPagination } from '../../../../../../hooks'; import { InstalledIntegrationsTable } from './components/installed_integrations_table'; import { useInstalledIntegrations } from './hooks/use_installed_integrations'; -import { useUrlFilters } from './hooks/use_url_filters'; +import { useUrlFilters, useViewPolicies } from './hooks/use_url_filters'; import { InstalledIntegrationsSearchBar } from './components/installed_integrations_search_bar'; import type { InstalledPackageUIPackageListItem } from './types'; import { BulkActionContextProvider, useBulkActions } from './hooks/use_bulk_actions'; +import { PackagePoliciesPanel } from './components/package_policies_panel'; const ContentWrapper = styled.div` max-width: 1200px; @@ -28,6 +29,7 @@ const ContentWrapper = styled.div` const InstalledIntegrationsPageContent: React.FunctionComponent = () => { // State management const filters = useUrlFilters(); + const { selectedPackageViewPolicies } = useViewPolicies(); const pagination = useUrlPagination(); const { upgradingIntegrations, uninstallingIntegrations } = useBulkActions(); const { @@ -46,27 +48,40 @@ const InstalledIntegrationsPageContent: React.FunctionComponent = () => { const [selectedItems, setSelectedItems] = useState([]); + const viewPoliciesSelectedItem = useMemo( + () => + selectedPackageViewPolicies + ? installedPackages.find((item) => item.name === selectedPackageViewPolicies) + : null, + [selectedPackageViewPolicies, installedPackages] + ); + if (isInitialLoading) { return ; } return ( - - - - - + <> + + + + + + {viewPoliciesSelectedItem ? ( + + ) : null} + ); };