Skip to content

Commit

Permalink
Merge branch 'main' into rules-management-client-refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
jpdjere authored May 24, 2024
2 parents 13f247d + e7e0c86 commit 366bc6d
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 65 deletions.
72 changes: 71 additions & 1 deletion x-pack/plugins/fleet/public/applications/fleet/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,29 @@ describe('AppRoutes', () => {
{
description: 'with Fleet:Agents:Read it should render AgentsApp',
path: '/agents',
expectReadOnly: true,
expectApp: 'AgentsApp',
authz: {
fleet: {
readAgents: true,
},
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',
Expand All @@ -52,13 +68,28 @@ describe('AppRoutes', () => {
description: 'with Fleet:AgentPolicies:Read it should render AgentPolicyApp',
path: '/policies',
expectApp: 'AgentPolicyApp',
expectReadOnly: true,
authz: {
fleet: {
readAgentPolicies: true,
},
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',
Expand All @@ -72,6 +103,7 @@ describe('AppRoutes', () => {
{
description: 'with Fleet:Settings:Read it should render SettingsApp',
path: '/settings',
expectReadOnly: true,
expectApp: 'SettingsApp',
authz: {
fleet: {
Expand All @@ -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',
Expand All @@ -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(<AppRoutes setHeaderActionMenu={() => {}} />, {});
if (scenario.expectMissingPrivileges) {
Expand All @@ -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()
);
}
});
}
});
Expand Down
142 changes: 107 additions & 35 deletions x-pack/plugins/fleet/public/applications/fleet/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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 (
<TopNavMenu
appName={i18n.translate('xpack.fleet.appTitle', { defaultMessage: 'Fleet' })}
Expand All @@ -246,72 +276,114 @@ const FleetTopNav = memo(
}
);

const AppLayout: React.FC<{
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
isReadOnly?: boolean;
}> = ({ children, setHeaderActionMenu, isReadOnly }) => {
return (
<>
<FleetTopNav setHeaderActionMenu={setHeaderActionMenu} isReadOnly={isReadOnly} />
{children}
</>
);
};

export const AppRoutes = memo(
({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => {
const flyoutContext = useFlyoutContext();
const fleetStatus = useFleetStatus();

const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get();

const authz = useAuthz();

return (
<>
<FleetTopNav setHeaderActionMenu={setHeaderActionMenu} />

<Routes>
<Route path={FLEET_ROUTING_PATHS.agents}>
<Route path={FLEET_ROUTING_PATHS.agents} key={FLEET_ROUTING_PATHS.agents}>
{authz.fleet.readAgents ? (
<AgentsApp />
<AppLayout
setHeaderActionMenu={setHeaderActionMenu}
isReadOnly={!authz.fleet.allAgents}
>
<AgentsApp />
</AppLayout>
) : (
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Agents Read" />
</ErrorLayout>
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Agents Read" />
</ErrorLayout>
</AppLayout>
)}
</Route>

<Route path={FLEET_ROUTING_PATHS.policies}>
{authz.fleet.readAgentPolicies ? (
<AgentPolicyApp />
<AppLayout
setHeaderActionMenu={setHeaderActionMenu}
isReadOnly={!authz.fleet.allAgentPolicies}
>
<AgentPolicyApp />
</AppLayout>
) : (
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError
error="MISSING_PRIVILEGES"
requiredFleetRole="Agent policies Read"
/>
</ErrorLayout>
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError
error="MISSING_PRIVILEGES"
requiredFleetRole="Agent policies Read"
/>
</ErrorLayout>
</AppLayout>
)}
</Route>

<Route path={FLEET_ROUTING_PATHS.enrollment_tokens}>
{authz.fleet.allAgents ? (
<EnrollmentTokenListPage />
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<EnrollmentTokenListPage />
</AppLayout>
) : (
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Agents All" />
</ErrorLayout>
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Agents All" />
</ErrorLayout>
</AppLayout>
)}
</Route>
{agentTamperProtectionEnabled && (
<Route path={FLEET_ROUTING_PATHS.uninstall_tokens}>
{authz.fleet.allAgents ? (
<UninstallTokenListPage />
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<UninstallTokenListPage />
</AppLayout>
) : (
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Agents All" />
</ErrorLayout>
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Agents All" />
</ErrorLayout>
</AppLayout>
)}
</Route>
)}
<Route path={FLEET_ROUTING_PATHS.data_streams}>
<DataStreamApp />
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<DataStreamApp />
</AppLayout>
</Route>

<Route path={FLEET_ROUTING_PATHS.settings}>
{authz.fleet.readSettings ? (
<SettingsApp />
<AppLayout
setHeaderActionMenu={setHeaderActionMenu}
isReadOnly={!authz.fleet.allSettings}
>
<SettingsApp />
</AppLayout>
) : (
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Settings Read" />
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Settings Read" />
</AppLayout>
</ErrorLayout>
)}
</Route>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,22 @@ export const RuleDetailsRoute: React.FunctionComponent<RuleDetailsRouteProps> =
return null;
};

return rule && ruleType && actionTypes ? (
<>
{getLegacyUrlConflictCallout()}
<RuleDetails
rule={rule}
ruleType={ruleType}
actionTypes={actionTypes}
requestRefresh={requestRefresh}
refreshToken={refreshToken}
/>
</>
) : (
<CenterJustifiedSpinner />
);
if (rule && ruleType && actionTypes) {
return (
<>
{getLegacyUrlConflictCallout()}
<RuleDetails
rule={rule}
ruleType={ruleType}
actionTypes={actionTypes}
requestRefresh={requestRefresh}
refreshToken={refreshToken}
/>
</>
);
}

return <CenterJustifiedSpinner />;
};

export async function getRuleData(
Expand Down
Loading

0 comments on commit 366bc6d

Please sign in to comment.