[8.10] [Profiling] display delete instruction page when old data is found (#164855) (#165071)

# Backport

This will backport the following commits from `main` to `8.10`:
- [[Profiling] display delete instruction page when old data is found
(#164855)](https://github.com/elastic/kibana/pull/164855)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Cauê
Marcondes","email":"55978943+cauemarcondes@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-08-29T09:37:13Z","message":"[Profiling]
display delete instruction page when old data is found (#164855)\n\n-
**Upgrading from 8.8 -> 8.9**\r\nShows the Set up page because of
missing Collector and Symbolizer\r\nintegrations\r\n\r\n- **Upgrading
from 8.9 -> 8.10**\r\nShows the Set up page because Profiling was
installed with APM\r\nintegration\r\n---\r\n<img width=\"719\"
alt=\"Screenshot 2023-08-25 at 16 01
53\"\r\nsrc=\"15fee345-868e-4fe1-add9-b9a8e41cfd20\">\r\n\r\nUsers
with `Viewer` privileges will open the Profiling UI, as they
don't\r\nhave permission to call the profiling status API and display a
callout.\r\n<img width=\"950\" alt=\"Screenshot 2023-08-25 at 20 46
28\"\r\nsrc=\"f398ac30-41df-410e-9447-7469b97d46db\">","sha":"a5428c05970026cde5a4c99df98514cb022096a4","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","ci:build-cloud-image","ci:cloud-deploy","v8.10.0","v8.11.0"],"number":164855,"url":"https://github.com/elastic/kibana/pull/164855","mergeCommit":{"message":"[Profiling]
display delete instruction page when old data is found (#164855)\n\n-
**Upgrading from 8.8 -> 8.9**\r\nShows the Set up page because of
missing Collector and Symbolizer\r\nintegrations\r\n\r\n- **Upgrading
from 8.9 -> 8.10**\r\nShows the Set up page because Profiling was
installed with APM\r\nintegration\r\n---\r\n<img width=\"719\"
alt=\"Screenshot 2023-08-25 at 16 01
53\"\r\nsrc=\"15fee345-868e-4fe1-add9-b9a8e41cfd20\">\r\n\r\nUsers
with `Viewer` privileges will open the Profiling UI, as they
don't\r\nhave permission to call the profiling status API and display a
callout.\r\n<img width=\"950\" alt=\"Screenshot 2023-08-25 at 20 46
28\"\r\nsrc=\"f398ac30-41df-410e-9447-7469b97d46db\">","sha":"a5428c05970026cde5a4c99df98514cb022096a4"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/164855","number":164855,"mergeCommit":{"message":"[Profiling]
display delete instruction page when old data is found (#164855)\n\n-
**Upgrading from 8.8 -> 8.9**\r\nShows the Set up page because of
missing Collector and Symbolizer\r\nintegrations\r\n\r\n- **Upgrading
from 8.9 -> 8.10**\r\nShows the Set up page because Profiling was
installed with APM\r\nintegration\r\n---\r\n<img width=\"719\"
alt=\"Screenshot 2023-08-25 at 16 01
53\"\r\nsrc=\"15fee345-868e-4fe1-add9-b9a8e41cfd20\">\r\n\r\nUsers
with `Viewer` privileges will open the Profiling UI, as they
don't\r\nhave permission to call the profiling status API and display a
callout.\r\n<img width=\"950\" alt=\"Screenshot 2023-08-25 at 20 46
28\"\r\nsrc=\"f398ac30-41df-410e-9447-7469b97d46db\">","sha":"a5428c05970026cde5a4c99df98514cb022096a4"}}]}]
BACKPORT-->

Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2023-08-29 07:52:38 -04:00 committed by GitHub
parent 608855df48
commit b1c642c5a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 465 additions and 257 deletions

View file

@ -6,8 +6,7 @@
*/
import {
areResourcesSetupForAdmin,
areResourcesSetupForViewer,
areResourcesSetup,
createDefaultSetupState,
mergePartialSetupStates,
PartialSetupState,
@ -54,178 +53,129 @@ function createSettingsState(configured: boolean): PartialSetupState {
}
describe('Merging partial state operations', () => {
describe('Merge states', () => {
const defaultSetupState = createDefaultSetupState();
const defaultSetupState = createDefaultSetupState();
it('returns partial states with missing key', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCloudState(true),
createDataState(true),
]);
it('returns partial states with missing key', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCloudState(true),
createDataState(true),
]);
expect(mergedState.cloud.available).toEqual(true);
expect(mergedState.cloud.required).toEqual(true);
expect(mergedState.data.available).toEqual(true);
});
it('should deeply nested partial states with overlap', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
]);
expect(mergedState.policies.collector.installed).toEqual(true);
expect(mergedState.policies.symbolizer.installed).toEqual(true);
});
});
describe('For admin users', () => {
const defaultSetupState = createDefaultSetupState();
it('returns false when permission is not configured', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(false),
]);
expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy();
});
it('returns false when resource management is not enabled', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: false, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy();
});
it('returns false when resources are not created', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: true, created: false }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy();
});
it('returns false when settings are not configured', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: true, created: true }),
createSettingsState(false),
createPermissionState(true),
]);
expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy();
});
it('returns true when all checks are valid', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(false),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetupForAdmin(mergedState)).toBeTruthy();
});
it('returns false when collector is not found', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(false),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(false),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy();
});
it('returns false when symbolizer is not found', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(false),
createProfilingInApmPolicyState(false),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy();
});
it('returns false when profiling is in APM server', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy();
});
expect(mergedState.cloud.available).toEqual(true);
expect(mergedState.cloud.required).toEqual(true);
expect(mergedState.data.available).toEqual(true);
});
describe('For viewer users', () => {
const defaultSetupState = createDefaultSetupState();
it('should deeply nested partial states with overlap', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
]);
it('returns false when collector is not installed', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(false),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(false),
]);
expect(mergedState.policies.collector.installed).toEqual(true);
expect(mergedState.policies.symbolizer.installed).toEqual(true);
});
it('returns false when permission is not configured', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(false),
]);
expect(areResourcesSetupForViewer(mergedState)).toBeFalsy();
});
expect(areResourcesSetup(mergedState)).toBeFalsy();
});
it('returns false when symbolizer is not installed', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(false),
createProfilingInApmPolicyState(false),
]);
it('returns false when resource management is not enabled', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: false, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetupForViewer(mergedState)).toBeFalsy();
});
expect(areResourcesSetup(mergedState)).toBeFalsy();
});
it('returns false when profiling is configured in APM policy', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
]);
it('returns false when resources are not created', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: true, created: false }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetupForViewer(mergedState)).toBeFalsy();
});
expect(areResourcesSetup(mergedState)).toBeFalsy();
});
it('returns true when all checks are valid', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(false),
]);
it('returns false when settings are not configured', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: true, created: true }),
createSettingsState(false),
createPermissionState(true),
]);
expect(areResourcesSetupForViewer(mergedState)).toBeTruthy();
});
expect(areResourcesSetup(mergedState)).toBeFalsy();
});
it('returns true when all checks are valid', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(false),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetup(mergedState)).toBeTruthy();
});
it('returns false when collector is not found', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(false),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(false),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetup(mergedState)).toBeFalsy();
});
it('returns false when symbolizer is not found', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(false),
createProfilingInApmPolicyState(false),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetup(mergedState)).toBeFalsy();
});
it('returns false when profiling is in APM server', () => {
const mergedState = mergePartialSetupStates(defaultSetupState, [
createCollectorPolicyState(true),
createSymbolizerPolicyState(true),
createProfilingInApmPolicyState(true),
createResourceState({ enabled: true, created: true }),
createSettingsState(true),
createPermissionState(true),
]);
expect(areResourcesSetup(mergedState)).toBeFalsy();
});
});

View file

@ -35,6 +35,7 @@ export interface SetupState {
};
resources: {
created: boolean;
pre_8_9_1_data: boolean;
};
settings: {
configured: boolean;
@ -71,6 +72,7 @@ export function createDefaultSetupState(): SetupState {
},
resources: {
created: false,
pre_8_9_1_data: false,
},
settings: {
configured: false,
@ -78,17 +80,11 @@ export function createDefaultSetupState(): SetupState {
};
}
export function areResourcesSetupForViewer(state: SetupState): boolean {
export function areResourcesSetup(state: SetupState): boolean {
return (
state.policies.collector.installed &&
state.policies.symbolizer.installed &&
!state.policies.apm.profilingEnabled
);
}
export function areResourcesSetupForAdmin(state: SetupState): boolean {
return (
areResourcesSetupForViewer(state) &&
!state.policies.apm.profilingEnabled &&
state.resource_management.enabled &&
state.resources.created &&
state.permissions.configured &&

View file

@ -24,6 +24,7 @@ export interface ProfilingStatusResponse {
};
resources: {
created: boolean;
pre_8_9_1_data: boolean;
};
}

View file

@ -10,9 +10,13 @@ describe('Home page with empty state', () => {
cy.loginAsElastic();
});
it('shows the empty state when Profiling has not been set up', () => {
it('shows Set up page when Profiling has not been set up', () => {
cy.intercept('GET', '/internal/profiling/setup/es_resources', {
fixture: 'es_resources_setup_false.json',
body: {
has_setup: false,
has_data: false,
pre_8_9_1_data: false,
},
}).as('getEsResources');
cy.visitKibana('/app/profiling');
cy.wait('@getEsResources');
@ -20,9 +24,13 @@ describe('Home page with empty state', () => {
cy.contains('Set up Universal Profiling');
});
it('shows the tutorial after Profiling has been set up', () => {
it('shows Add data page after Profiling has been set up', () => {
cy.intercept('GET', '/internal/profiling/setup/es_resources', {
fixture: 'es_resources_data_false.json',
body: {
has_setup: true,
has_data: false,
pre_8_9_1_data: false,
},
}).as('getEsResources');
cy.visitKibana('/app/profiling');
cy.wait('@getEsResources');
@ -34,4 +42,45 @@ describe('Home page with empty state', () => {
cy.contains('RPM Package');
cy.contains('Upload Symbols');
});
describe('Delete Data View', () => {
it('shows Delete page when setup is false', () => {
cy.intercept('GET', '/internal/profiling/setup/es_resources', {
body: {
has_setup: false,
has_data: true,
pre_8_9_1_data: true,
},
}).as('getEsResources');
cy.visitKibana('/app/profiling');
cy.wait('@getEsResources');
cy.contains('Delete existing profiling data');
});
it('shows Delete page when data pre 8.9.1 is still available and data is found', () => {
cy.intercept('GET', '/internal/profiling/setup/es_resources', {
body: {
has_setup: true,
has_data: true,
pre_8_9_1_data: true,
},
}).as('getEsResources');
cy.visitKibana('/app/profiling');
cy.wait('@getEsResources');
cy.contains('Delete existing profiling data');
});
it('shows Delete page when data pre 8.9.1 is still available and data is not found', () => {
cy.intercept('GET', '/internal/profiling/setup/es_resources', {
body: {
has_setup: true,
has_data: false,
pre_8_9_1_data: true,
},
}).as('getEsResources');
cy.visitKibana('/app/profiling');
cy.wait('@getEsResources');
cy.contains('Delete existing profiling data');
});
});
});

View file

@ -19,6 +19,21 @@ describe('Home page', () => {
cy.loginAsElastic();
});
it('opens Profiling UI when user does not have privileges', () => {
cy.intercept('GET', '/internal/profiling/setup/es_resources', {
body: {
has_setup: true,
pre_8_9_1_data: false,
has_data: true,
unauthorized: true,
},
}).as('getEsResources');
cy.visitKibana('/app/profiling', { rangeFrom, rangeTo });
cy.wait('@getEsResources');
cy.contains('Top 46');
cy.contains('User privilege limitation');
});
it('navigates through the tabs', () => {
cy.visitKibana('/app/profiling', { rangeFrom, rangeTo });
cy.url().should('include', '/app/profiling/stacktraces/threads');

View file

@ -1,4 +0,0 @@
{
"has_setup": true,
"has_data": false
}

View file

@ -1,4 +0,0 @@
{
"has_setup": false,
"has_data": false
}

View file

@ -25,6 +25,7 @@ import { ProfilingPluginPublicSetupDeps, ProfilingPluginPublicStartDeps } from '
import { ProfilingHeaderActionMenu } from './components/profiling_header_action_menu';
import { RouterErrorBoundary } from './routing/router_error_boundary';
import { LicenseProvider } from './components/contexts/license/license_context';
import { ProfilingSetupStatusContextProvider } from './components/contexts/profiling_setup_status/profiling_setup_status_context';
interface Props {
profilingFetchServices: Services;
@ -89,21 +90,23 @@ function App({
<RouterErrorBoundary>
<TimeRangeContextProvider>
<ProfilingDependenciesContextProvider value={profilingDependencies}>
<LicenseProvider>
<>
<CheckSetup>
<RedirectWithDefaultDateRange>
<RouteBreadcrumbsContextProvider>
<RouteRenderer />
</RouteBreadcrumbsContextProvider>
</RedirectWithDefaultDateRange>
</CheckSetup>
<MountProfilingActionMenu
setHeaderActionMenu={setHeaderActionMenu}
theme$={theme$}
/>
</>
</LicenseProvider>
<ProfilingSetupStatusContextProvider>
<LicenseProvider>
<>
<CheckSetup>
<RedirectWithDefaultDateRange>
<RouteBreadcrumbsContextProvider>
<RouteRenderer />
</RouteBreadcrumbsContextProvider>
</RedirectWithDefaultDateRange>
</CheckSetup>
<MountProfilingActionMenu
setHeaderActionMenu={setHeaderActionMenu}
theme$={theme$}
/>
</>
</LicenseProvider>
</ProfilingSetupStatusContextProvider>
</ProfilingDependenciesContextProvider>
</TimeRangeContextProvider>
</RouterErrorBoundary>

View file

@ -26,12 +26,14 @@ import { useLicenseContext } from './contexts/license/use_license_context';
import { useProfilingDependencies } from './contexts/profiling_dependencies/use_profiling_dependencies';
import { LicensePrompt } from './license_prompt';
import { ProfilingAppPageTemplate } from './profiling_app_page_template';
import { useProfilingSetupStatus } from './contexts/profiling_setup_status/use_profiling_setup_status';
export function CheckSetup({ children }: { children: React.ReactElement }) {
const {
start: { core },
services: { fetchHasSetup, postSetupResources },
} = useProfilingDependencies();
const { setProfilingSetupStatus } = useProfilingSetupStatus();
const license = useLicenseContext();
const router = useProfilingRouter();
const history = useHistory();
@ -47,6 +49,10 @@ export function CheckSetup({ children }: { children: React.ReactElement }) {
[fetchHasSetup]
);
if (status === AsyncStatus.Settled) {
setProfilingSetupStatus(data);
}
const http = useAutoAbortedHttpClient([]);
if (!license?.hasAtLeast('enterprise')) {
@ -79,7 +85,10 @@ export function CheckSetup({ children }: { children: React.ReactElement }) {
}
const displaySetupScreen =
(status === AsyncStatus.Settled && data?.has_setup !== true) || !!error;
(status === AsyncStatus.Settled &&
data?.has_setup !== true &&
data?.pre_8_9_1_data === false) ||
!!error;
if (displaySetupScreen) {
return (
@ -193,19 +202,27 @@ export function CheckSetup({ children }: { children: React.ReactElement }) {
);
}
const displayAddDataInstructions =
status === AsyncStatus.Settled && data?.has_setup === true && data?.has_data === false;
const displayUi =
// Display UI if there's data or if the user is opening the add data instruction page.
// does not use profiling router because that breaks as at this point the route might not have all required params
data?.has_data === true || history.location.pathname === '/add-data-instructions';
(data?.has_data === true && data?.pre_8_9_1_data === false) ||
history.location.pathname === '/add-data-instructions' ||
history.location.pathname === '/delete_data_instructions';
if (displayUi) {
return children;
}
if (displayAddDataInstructions) {
if (data?.pre_8_9_1_data === true) {
// If the cluster still has data pre 8.9.1 version, redirect to deleting instructions
router.push('/delete_data_instructions', {
path: {},
query: {},
});
return null;
}
if (status === AsyncStatus.Settled && data?.has_setup === true && data?.has_data === false) {
// when there's no data redirect the user to the add data instructions page
router.push('/add-data-instructions', {
path: {},

View file

@ -0,0 +1,35 @@
/*
* 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 } from 'react';
import { ProfilingSetupStatus } from '../../../services';
export const ProfilingSetupStatusContext = React.createContext<
| {
profilingSetupStatus: ProfilingSetupStatus | undefined;
setProfilingSetupStatus: React.Dispatch<
React.SetStateAction<ProfilingSetupStatus | undefined>
>;
}
| undefined
>(undefined);
export function ProfilingSetupStatusContextProvider({
children,
}: {
children: React.ReactElement;
}) {
const [profilingSetupStatus, setProfilingSetupStatus] = useState<
ProfilingSetupStatus | undefined
>();
return (
<ProfilingSetupStatusContext.Provider value={{ profilingSetupStatus, setProfilingSetupStatus }}>
{children}
</ProfilingSetupStatusContext.Provider>
);
}

View file

@ -0,0 +1,17 @@
/*
* 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 { useContext } from 'react';
import { ProfilingSetupStatusContext } from './profiling_setup_status_context';
export function useProfilingSetupStatus() {
const context = useContext(ProfilingSetupStatusContext);
if (!context) {
throw new Error('ProfilingSetupStatusContext not found');
}
return context;
}

View file

@ -8,6 +8,7 @@
import {
EuiBetaBadge,
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
@ -20,6 +21,8 @@ import { useHistory } from 'react-router-dom';
import { NoDataPageProps } from '@kbn/shared-ux-page-no-data-types';
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
import { PrimaryProfilingSearchBar } from './primary_profiling_search_bar';
import { useLocalStorage } from '../../hooks/use_local_storage';
import { useProfilingSetupStatus } from '../contexts/profiling_setup_status/use_profiling_setup_status';
export const PROFILING_FEEDBACK_LINK = 'https://ela.st/profiling-feedback';
@ -46,6 +49,12 @@ export function ProfilingAppPageTemplate({
start: { observabilityShared },
} = useProfilingDependencies();
const [privilegesWarningDismissed, setPrivilegesWarningDismissed] = useLocalStorage(
'profiling.privilegesWarningDismissed',
false
);
const { profilingSetupStatus } = useProfilingSetupStatus();
const { PageTemplate: ObservabilityPageTemplate } = observabilityShared.navigation;
const history = useHistory();
@ -110,6 +119,32 @@ export function ProfilingAppPageTemplate({
</EuiPanel>
</EuiFlexItem>
)}
{profilingSetupStatus?.unauthorized === true && privilegesWarningDismissed !== true ? (
<EuiFlexItem grow={false}>
<EuiCallOut
iconType="warning"
title={i18n.translate('xpack.profiling.privilegesWarningTitle', {
defaultMessage: 'User privilege limitation',
})}
>
<p>
{i18n.translate('xpack.profiling.privilegesWarningDescription', {
defaultMessage:
'Due to privileges issues we could not check the Universal Profiling status. If you encounter any issues or if data fails to load, please contact your administrator for assistance.',
})}
</p>
<EuiButton
onClick={() => {
setPrivilegesWarningDismissed(true);
}}
>
{i18n.translate('xpack.profiling.dismissPrivilegesCallout', {
defaultMessage: 'Dismiss',
})}
</EuiButton>
</EuiCallOut>
</EuiFlexItem>
) : null}
<EuiFlexItem>{children}</EuiFlexItem>
</EuiFlexGroup>
</ObservabilityPageTemplate>

View file

@ -0,0 +1,57 @@
/*
* 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 { useState, useEffect, useMemo } from 'react';
export function useLocalStorage<T>(key: string, defaultValue: T) {
// This is necessary to fix a race condition issue.
// It guarantees that the latest value will be always returned after the value is updated
const [storageUpdate, setStorageUpdate] = useState(0);
const item = useMemo(() => {
return getFromStorage(key, defaultValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key, storageUpdate, defaultValue]);
const saveToStorage = (value: T) => {
if (value === undefined) {
window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, JSON.stringify(value));
setStorageUpdate(storageUpdate + 1);
}
};
useEffect(() => {
function onUpdate(event: StorageEvent) {
if (event.key === key) {
setStorageUpdate(storageUpdate + 1);
}
}
window.addEventListener('storage', onUpdate);
return () => {
window.removeEventListener('storage', onUpdate);
};
}, [key, setStorageUpdate, storageUpdate]);
return [item, saveToStorage] as const;
}
function getFromStorage<T>(keyName: string, defaultValue: T) {
const storedItem = window.localStorage.getItem(keyName);
if (storedItem !== null) {
try {
return JSON.parse(storedItem) as T;
} catch (err) {
window.localStorage.removeItem(keyName);
// eslint-disable-next-line no-console
console.log(`Unable to decode: ${keyName}`);
}
}
return defaultValue;
}

View file

@ -27,6 +27,7 @@ import { AddDataTabs, AddDataView } from '../views/add_data_view';
import { StackTracesView } from '../views/stack_traces_view';
import { StorageExplorerView } from '../views/storage_explorer';
import { RouteBreadcrumb } from './route_breadcrumb';
import { DeleteDataView } from '../views/delete_data_view';
const routes = {
'/': {
@ -62,6 +63,9 @@ const routes = {
},
},
},
'/delete_data_instructions': {
element: <DeleteDataView />,
},
'/': {
children: {
'/stacktraces/{topNType}': {

View file

@ -18,6 +18,13 @@ import { TopNResponse } from '../common/topn';
import type { SetupDataCollectionInstructions } from '../server/lib/setup/get_setup_instructions';
import { AutoAbortedHttpService } from './hooks/use_auto_aborted_http_client';
export interface ProfilingSetupStatus {
has_setup: boolean;
has_data: boolean;
pre_8_9_1_data: boolean;
unauthorized?: boolean;
}
export interface Services {
fetchTopN: (params: {
http: AutoAbortedHttpService;
@ -40,9 +47,7 @@ export interface Services {
timeTo: number;
kuery: string;
}) => Promise<ElasticFlameGraph>;
fetchHasSetup: (params: {
http: AutoAbortedHttpService;
}) => Promise<{ has_setup: boolean; has_data: boolean }>;
fetchHasSetup: (params: { http: AutoAbortedHttpService }) => Promise<ProfilingSetupStatus>;
postSetupResources: (params: { http: AutoAbortedHttpService }) => Promise<void>;
setupDataCollectionInstructions: (params: {
http: AutoAbortedHttpService;
@ -101,10 +106,7 @@ export function getServices(): Services {
return createFlameGraph(baseFlamegraph);
},
fetchHasSetup: async ({ http }) => {
const hasSetup = (await http.get(paths.HasSetupESResources, {})) as {
has_setup: boolean;
has_data: boolean;
};
const hasSetup = (await http.get(paths.HasSetupESResources, {})) as ProfilingSetupStatus;
return hasSetup;
},
postSetupResources: async ({ http }) => {

View file

@ -0,0 +1,42 @@
/*
* 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 { EuiCard, EuiIcon, EuiLink } from '@elastic/eui';
import React from 'react';
import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template';
export function DeleteDataView() {
const {
start: {
core: { docLinks },
},
} = useProfilingDependencies();
return (
<ProfilingAppPageTemplate tabs={[]} restrictWidth hideSearchBar>
<div style={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}>
<EuiCard
style={{ flexGrow: 0, maxWidth: '500px' }}
icon={<EuiIcon color="danger" size="xxl" type="warning" />}
title="You have existing profiling data"
description="To proceed with the Universal Profiling setup, please delete existing profiling data following the steps described in the link below."
footer={
<div>
<EuiLink
href={`${docLinks.ELASTIC_WEBSITE_URL}/guide/en/observability/${docLinks.DOC_LINK_VERSION}/profiling-upgrade.html#profiling-delete-data`}
target="_blank"
>
Delete existing profiling data
</EuiLink>
</div>
}
/>
</div>
</ProfilingAppPageTemplate>
);
}

View file

@ -42,6 +42,7 @@ export async function validateResourceManagement({
},
resources: {
created: statusResponse.resources.created,
pre_8_9_1_data: statusResponse.resources.pre_8_9_1_data,
},
};
}

View file

@ -9,8 +9,7 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import {
areResourcesSetupForAdmin,
areResourcesSetupForViewer,
areResourcesSetup,
createDefaultSetupState,
mergePartialSetupStates,
} from '../../common/setup';
@ -88,58 +87,47 @@ export function registerSetupRoute({
},
});
}
const verifyFunctionsForViewer = [
const verifyFunctions = [
validateMaximumBuckets,
validateResourceManagement,
validateSecurityRole,
validateCollectorPackagePolicy,
validateSymbolizerPackagePolicy,
validateProfilingInApmPackagePolicy,
];
const partialStatesForViewer = await Promise.all([
...verifyFunctionsForViewer.map((fn) => fn(setupOptions)),
const partialStates = await Promise.all([
...verifyFunctions.map((fn) => fn(setupOptions)),
hasProfilingData({
...setupOptions,
client: clientWithProfilingAuth,
}),
]);
const mergedStateForViewer = mergePartialSetupStates(state, partialStatesForViewer);
/*
* We need to split the verification steps
* because of users with viewer privileges
* cannot get the cluster settings
*/
if (areResourcesSetupForViewer(mergedStateForViewer)) {
return response.ok({
body: {
has_setup: true,
has_data: mergedStateForViewer.data.available,
},
});
}
/**
* Performe advanced verification in case the first step failed.
*/
const verifyFunctionsForAdmin = [
validateMaximumBuckets,
validateResourceManagement,
validateSecurityRole,
];
const partialStatesForAdmin = await Promise.all(
verifyFunctionsForAdmin.map((fn) => fn(setupOptions))
);
const mergedState = mergePartialSetupStates(mergedStateForViewer, partialStatesForAdmin);
const mergedState = mergePartialSetupStates(state, partialStates);
return response.ok({
body: {
has_setup: areResourcesSetupForAdmin(mergedState),
has_setup: areResourcesSetup(mergedState),
has_data: mergedState.data.available,
pre_8_9_1_data: mergedState.resources.pre_8_9_1_data,
},
});
} catch (error) {
// We cannot fully check the status of all resources
// to make sure Profiling has been set up and has data
// for users with monitor privileges. This privileges
// is needed to call the profiling ES plugin for example.
if (error?.meta?.statusCode === 403) {
return response.ok({
body: {
has_setup: true,
pre_8_9_1_data: false,
has_data: true,
unauthorized: true,
},
});
}
return handleRouteHandlerError({
error,
logger,
@ -192,32 +180,36 @@ export function registerSetupRoute({
const partialStates = await Promise.all(
[
validateCollectorPackagePolicy,
validateMaximumBuckets,
validateResourceManagement,
validateSecurityRole,
validateMaximumBuckets,
validateCollectorPackagePolicy,
validateSymbolizerPackagePolicy,
validateProfilingInApmPackagePolicy,
].map((fn) => fn(setupOptions))
);
const mergedState = mergePartialSetupStates(state, partialStates);
const executeFunctions = [
...(mergedState.policies.collector.installed ? [] : [createCollectorPackagePolicy]),
...(mergedState.policies.symbolizer.installed ? [] : [createSymbolizerPackagePolicy]),
...(mergedState.policies.apm.profilingEnabled
? [removeProfilingFromApmPackagePolicy]
: []),
const executeAdminFunctions = [
...(mergedState.resource_management.enabled ? [] : [enableResourceManagement]),
...(mergedState.permissions.configured ? [] : [setSecurityRole]),
...(mergedState.settings.configured ? [] : [setMaximumBuckets]),
];
if (!executeFunctions.length) {
const executeViewerFunctions = [
...(mergedState.policies.collector.installed ? [] : [createCollectorPackagePolicy]),
...(mergedState.policies.symbolizer.installed ? [] : [createSymbolizerPackagePolicy]),
...(mergedState.policies.apm.profilingEnabled
? [removeProfilingFromApmPackagePolicy]
: []),
];
if (!executeAdminFunctions.length && !executeViewerFunctions.length) {
return response.ok();
}
await Promise.all(executeFunctions.map((fn) => fn(setupOptions)));
await Promise.all(executeAdminFunctions.map((fn) => fn(setupOptions)));
await Promise.all(executeViewerFunctions.map((fn) => fn(setupOptions)));
if (dependencies.telemetryUsageCounter) {
dependencies.telemetryUsageCounter.incrementCounter({