mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# 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:
parent
608855df48
commit
b1c642c5a4
18 changed files with 465 additions and 257 deletions
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -24,6 +24,7 @@ export interface ProfilingStatusResponse {
|
|||
};
|
||||
resources: {
|
||||
created: boolean;
|
||||
pre_8_9_1_data: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"has_setup": true,
|
||||
"has_data": false
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"has_setup": false,
|
||||
"has_data": false
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
57
x-pack/plugins/profiling/public/hooks/use_local_storage.ts
Normal file
57
x-pack/plugins/profiling/public/hooks/use_local_storage.ts
Normal 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;
|
||||
}
|
|
@ -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}': {
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue