From ce0f0871634bcb19d6a76e294c30801cb062aab6 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 24 May 2024 08:22:05 -0400 Subject: [PATCH 1/2] [Fleet] Add Readonly tooltip when user do not have all privileges (#184166) --- .../public/applications/fleet/app.test.tsx | 72 ++++++++- .../fleet/public/applications/fleet/app.tsx | 142 +++++++++++++----- 2 files changed, 178 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.test.tsx index a79946e49f366..e76a879f7154a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.test.tsx @@ -31,6 +31,8 @@ describe('AppRoutes', () => { { description: 'with Fleet:Agents:Read it should render AgentsApp', path: '/agents', + expectReadOnly: true, + expectApp: 'AgentsApp', authz: { fleet: { readAgents: true, @@ -38,6 +40,20 @@ describe('AppRoutes', () => { integrations: {}, }, }, + { + description: + 'with Fleet:Agents:Read and Fleet:Agents:All it should render AgentsApp without readonly', + path: '/agents', + expectReadOnly: false, + expectApp: 'AgentsApp', + authz: { + fleet: { + allAgents: true, + readAgents: true, + }, + integrations: {}, + }, + }, { description: 'without Fleet:Agents:Read it should not render AgentsApp', path: '/agents', @@ -52,6 +68,7 @@ describe('AppRoutes', () => { description: 'with Fleet:AgentPolicies:Read it should render AgentPolicyApp', path: '/policies', expectApp: 'AgentPolicyApp', + expectReadOnly: true, authz: { fleet: { readAgentPolicies: true, @@ -59,6 +76,20 @@ describe('AppRoutes', () => { integrations: {}, }, }, + { + description: + 'with Fleet:AgentPolicies:Read and Fleet:AgentPolicies:All it should render AgentPolicyApp without readonly', + path: '/policies', + expectApp: 'AgentPolicyApp', + expectReadOnly: false, + authz: { + fleet: { + allAgentPolicies: true, + readAgentPolicies: true, + }, + integrations: {}, + }, + }, { description: 'without Fleet:AgentPolicies:Read it should not render AgentPolicyApp', path: '/policies', @@ -72,6 +103,7 @@ describe('AppRoutes', () => { { description: 'with Fleet:Settings:Read it should render SettingsApp', path: '/settings', + expectReadOnly: true, expectApp: 'SettingsApp', authz: { fleet: { @@ -80,6 +112,20 @@ describe('AppRoutes', () => { integrations: {}, }, }, + { + description: + 'with Fleet:Settings:Read and Fleet:Settings:All it should render SettingsApp without readonly', + path: '/settings', + expectReadOnly: false, + expectApp: 'SettingsApp', + authz: { + fleet: { + readSettings: true, + allSettings: true, + }, + integrations: {}, + }, + }, { description: 'without Fleet:Settings:Read it should not render SettingsApp', path: '/settings', @@ -95,7 +141,7 @@ describe('AppRoutes', () => { it(scenario.description, () => { jest.mocked(useAuthz).mockReturnValue(scenario.authz as any); const testRenderer = createFleetTestRendererMock(); - testRenderer.startServices.navigation.ui.TopNavMenu = () => null as any; + testRenderer.startServices.navigation.ui.TopNavMenu = jest.fn().mockReturnValue(null); testRenderer.history.push(`/mock${scenario.path}`); const result = testRenderer.render( {}} />, {}); if (scenario.expectMissingPrivileges) { @@ -106,6 +152,30 @@ describe('AppRoutes', () => { if (scenario.expectApp) { expect(result.queryByText(scenario.expectApp)).not.toBeNull(); } + + if (scenario.expectReadOnly) { + expect(testRenderer.startServices.navigation.ui.TopNavMenu).toBeCalledWith( + expect.objectContaining({ + config: expect.arrayContaining([ + expect.objectContaining({ + label: 'Read-only', + }), + ]), + }), + expect.anything() + ); + } else { + expect(testRenderer.startServices.navigation.ui.TopNavMenu).not.toBeCalledWith( + expect.objectContaining({ + config: expect.arrayContaining([ + expect.objectContaining({ + label: 'Read-only', + }), + ]), + }), + expect.anything() + ); + } }); } }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 99a0d8ffbdbbe..faaf08adf4dbb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -7,7 +7,7 @@ import React, { memo, useEffect, useState } from 'react'; import type { AppMountParameters } from '@kbn/core/public'; -import { EuiPortal } from '@elastic/eui'; +import { EuiPortal, useEuiTheme } from '@elastic/eui'; import type { History } from 'history'; import { Redirect, useRouteMatch } from 'react-router-dom'; import { Router, Routes, Route } from '@kbn/shared-ux-router'; @@ -17,6 +17,7 @@ import { i18n } from '@kbn/i18n'; import useObservable from 'react-use/lib/useObservable'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { css } from '@emotion/css'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; @@ -222,20 +223,49 @@ export const FleetAppContext: React.FC<{ ); const FleetTopNav = memo( - ({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => { + ({ + setHeaderActionMenu, + isReadOnly, + }: { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + isReadOnly?: boolean; + }) => { const services = useStartServices(); + const { euiTheme } = useEuiTheme(); + + const readOnlyBtnClass = React.useMemo(() => { + return css` + color: ${euiTheme.colors.text}; + `; + }, [euiTheme]); const { TopNavMenu } = services.navigation.ui; - const topNavConfig: TopNavMenuData[] = [ - { - label: i18n.translate('xpack.fleet.appNavigation.sendFeedbackButton', { - defaultMessage: 'Send feedback', + const topNavConfig: TopNavMenuData[] = []; + + if (isReadOnly) { + topNavConfig.push({ + label: i18n.translate('xpack.fleet.appNavigation.readOnlyBtn', { + defaultMessage: 'Read-only', + }), + disableButton: true, + className: readOnlyBtnClass, + iconType: 'glasses', + tooltip: i18n.translate('xpack.fleet.appNavigation.readOnlyTooltip', { + defaultMessage: + "You can view most Fleet settings, but your current privileges don't allow you to perform all actions.", }), - iconType: 'popout', - run: () => window.open(FEEDBACK_URL), - }, - ]; + run: () => {}, + }); + } + topNavConfig.push({ + label: i18n.translate('xpack.fleet.appNavigation.sendFeedbackButton', { + defaultMessage: 'Send feedback', + }), + iconType: 'popout', + run: () => window.open(FEEDBACK_URL), + }); + return ( = ({ children, setHeaderActionMenu, isReadOnly }) => { + return ( + <> + + {children} + + ); +}; + export const AppRoutes = memo( ({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => { const flyoutContext = useFlyoutContext(); const fleetStatus = useFleetStatus(); + const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get(); const authz = useAuthz(); return ( <> - - - + {authz.fleet.readAgents ? ( - + + + ) : ( - - - + + + + + )} {authz.fleet.readAgentPolicies ? ( - + + + ) : ( - - - + + + + + )} {authz.fleet.allAgents ? ( - + + + ) : ( - - - + + + + + )} {agentTamperProtectionEnabled && ( {authz.fleet.allAgents ? ( - + + + ) : ( - - - + + + + + )} )} - + + + {authz.fleet.readSettings ? ( - + + + ) : ( - + + + )} From e7e0c864e70842968c6b8b1c09737fb8478a6d4e Mon Sep 17 00:00:00 2001 From: Umberto Pepato Date: Fri, 24 May 2024 14:24:28 +0200 Subject: [PATCH 2/2] [ResponseOps][Rules] Fix flaky rule details page tests (#183888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Prevents the rule details page's lazyly loaded components from re-triggering the fallback state of the top-level `` that was causing a _Flash of Content_, the likely cause of #172941 and #173008. ## Details
Slowed down screen capture of the loading flash https://github.com/elastic/kibana/assets/18363145/8ab0bd7c-bcef-417f-9930-ef03aeec6e8b
My guess is that in the flaky test runs the `Edit` button was pressed just before the component was temporarily removed from the render tree and therefore the flyout couldn't render correctly. ## To verify 1. Create any type of rule from `Stack Management` 2. Navigate to that rule's detail page (`Stack Management` > `Rules` > Click on rule in list) 3. Verify that the page header only loads once without going back to the loading indicator (throttling the network from the DevTools or capturing the screen and playing back the video slowed down might help). Fixes #172941 Fixes #173008 --- .../components/rule_details_route.tsx | 30 ++++++++++--------- .../rule_details/components/rule_route.tsx | 26 ++++++++-------- .../apps/triggers_actions_ui/details.ts | 4 +-- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx index 116c97ef905d7..e195b12eea9e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx @@ -130,20 +130,22 @@ export const RuleDetailsRoute: React.FunctionComponent = return null; }; - return rule && ruleType && actionTypes ? ( - <> - {getLegacyUrlConflictCallout()} - - - ) : ( - - ); + if (rule && ruleType && actionTypes) { + return ( + <> + {getLegacyUrlConflictCallout()} + + + ); + } + + return ; }; export async function getRuleData( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx index f1df6af277549..2c7c5a7ac9267 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ToastsApi } from '@kbn/core/public'; -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React, { useState, useEffect, useRef, useCallback, Suspense } from 'react'; import { Rule, RuleSummary, RuleType } from '../../../../types'; import { ComponentOpts as RuleApis, @@ -76,17 +76,19 @@ export const RuleRoute: React.FunctionComponent = ({ ); return ruleSummary ? ( - + }> + + ) : ( ); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index b71f659b10baa..546ee11fd4ea8 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -319,9 +319,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/172941 - // FLAKY: https://github.com/elastic/kibana/issues/173008 - describe.skip('Edit rule button', function () { + describe('Edit rule button', function () { const ruleName = uuidv4(); const updatedRuleName = `Changed Rule Name ${ruleName}`;