Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"10.9.0": [
{
"title": "Test Search",
"description": "Test saved search",
"tabs": [
{
"id": "tab-1",
"label": "Main Tab",
"attributes": {
"columns": ["field1", "field2"],
"sort": [["@timestamp", "desc"]],
"grid": {},
"hideChart": false,
"isTextBasedQuery": false,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
},
"rowHeight": 3,
"density": "normal"
}
}
]
}
],
"10.10.0": [
{
"title": "Test Search",
"description": "Test saved search",
"tabs": [
{
"id": "tab-1",
"label": "Main Tab",
"attributes": {
"columns": ["field1", "field2"],
"sort": [["@timestamp", "desc"]],
"grid": {},
"hideChart": false,
"isTextBasedQuery": false,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
},
"rowHeight": 3,
"density": "normal"
}
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"risk-engine-configuration": "9d54f733fb2bd08978c7059d71e77741574dc2616823745501742d34816a408c",
"rules-settings": "436a36535b5d57ea1f7cbaaa37887ed5ddac8c3dea30c9fd98b3931ae87dfe1a",
"sample-data-telemetry": "4c102e89bdcaee1ccc887d1709c7e176c05f25b4c5ac14c3d013b58fbfd806ac",
"search": "431e34cbf3aadc050c4f5e23e2e13da977d1d37468f3651499d722c207723c4c",
"search": "6b482417fcbb8dad21a89c4a05b6701bbb39cc96933a8e82d8317e20e7f2eec7",
"search-session": "95e62da1c06afde503c7d12efebc7df2102b9cb8bead1f50ca5c6ab8f4b67c26",
"search-telemetry": "c152fc7e66d5ac7907e81c0926be9c219a15181e10b418b2fbb86bab2760627c",
"search_playground": "97895cb5356dd7dad771b6d72b5c236c7c229c012545d56bf6e849984512c9b1",
Expand Down Expand Up @@ -1026,6 +1026,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"search|global: ce649a79d99c5ff5eb68d544635428ef87946d84",
"search|mappings: 432d4dfdb5a33ce29d00ccdcfcda70d7c5f94b52",
"search|schemas: 8d6477e08dfdf20335752a69994646f9da90741f",
"search|10.10.0: 479252675efede136671875406da242ba00df105c02fcab784c22c241bc2e152",
"search|10.9.0: 557d8a40f3cd758fb4da9afba44e827a8c18b63ba140af871cf4a815f8e5e869",
"search|10.8.0: 76274f35cc139d5e208236bb92c859dd29e27ade181950a9f0bc3e95220c86dc",
"search|10.7.0: 03bcc899ac7be8e0a88520ae8fc091fc6ea37b231848dcbc0119b7425f36dd0e",
Expand Down Expand Up @@ -1360,7 +1361,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"risk-engine-configuration": "10.4.0",
"rules-settings": "10.1.0",
"sample-data-telemetry": "10.0.0",
"search": "10.9.0",
"search": "10.10.0",
"search-session": "10.0.0",
"search-telemetry": "10.0.0",
"search_playground": "10.1.0",
Expand Down Expand Up @@ -1508,7 +1509,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"risk-engine-configuration": "10.4.0",
"rules-settings": "10.1.0",
"sample-data-telemetry": "0.0.0",
"search": "10.9.0",
"search": "10.10.0",
"search-session": "8.6.0",
"search-telemetry": "7.12.0",
"search_playground": "10.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import type { AggregateQuery, Filter, ProjectRouting, Query, TimeRange } from '@kbn/es-query';
import { waitFor } from '@testing-library/react';
import { BehaviorSubject, Subject, skip } from 'rxjs';
import { fetch$ } from './fetch';
Expand Down Expand Up @@ -376,5 +376,102 @@ describe('onFetchContextChanged', () => {
subscription.unsubscribe();
});
});

describe('local and parent project routing', () => {
const api = {
parentApi: {
...parentApi,
projectRouting$: new BehaviorSubject<ProjectRouting | undefined>('ALL'),
},
projectRouting$: new BehaviorSubject<ProjectRouting | undefined>('_alias:_origin'),
};

test('should emit on subscribe (projectRouting is local projectRouting)', async () => {
const subscription = fetch$(api).subscribe(onFetchMock);
await waitFor(() => {
expect(onFetchMock).toHaveBeenCalledTimes(1);
});
const fetchContext = onFetchMock.mock.calls[0][0];
expect(fetchContext.projectRouting).toEqual('_alias:_origin');
subscription.unsubscribe();
});

test('should emit once on local project routing change', async () => {
const subscription = fetch$(api).pipe(skip(1)).subscribe(onFetchMock);
await waitForSearchSession();

api.projectRouting$.next('project1');
await waitFor(() => {
expect(onFetchMock).toHaveBeenCalledTimes(1);
});

const fetchContext = onFetchMock.mock.calls[0][0];
expect(fetchContext.projectRouting).toEqual('project1');
subscription.unsubscribe();
});

test('should not emit on parent project routing change', async () => {
const subscription = fetch$(api).pipe(skip(1)).subscribe(onFetchMock);
await waitForSearchSession();
expect(onFetchMock).not.toHaveBeenCalled();

api.parentApi.projectRouting$.next('project2');
await new Promise((resolve) => setTimeout(resolve, 100));
expect(onFetchMock).not.toHaveBeenCalled();
subscription.unsubscribe();
});

test('should emit once when local project routing is cleared (projectRouting is parent projectRouting)', async () => {
// Reset parent projectRouting to 'ALL' in case previous test changed it
api.parentApi.projectRouting$.next('ALL');
api.projectRouting$.next('_alias:_origin'); // Reset local to initial value

const subscription = fetch$(api).pipe(skip(1)).subscribe(onFetchMock);
await waitForSearchSession();
expect(onFetchMock).not.toHaveBeenCalled();

api.projectRouting$.next(undefined);
await waitFor(() => {
expect(onFetchMock).toHaveBeenCalledTimes(1);
});
const fetchContext = onFetchMock.mock.calls[0][0];
expect(fetchContext.projectRouting).toEqual('ALL');
subscription.unsubscribe();
});
});

describe('only parent project routing', () => {
const api = {
parentApi: {
...parentApi,
projectRouting$: new BehaviorSubject<ProjectRouting | undefined>('ALL'),
},
};

test('should emit on subscribe (projectRouting is parent projectRouting)', async () => {
const subscription = fetch$(api).subscribe(onFetchMock);
await waitFor(() => {
expect(onFetchMock).toHaveBeenCalledTimes(1);
});
const fetchContext = onFetchMock.mock.calls[0][0];
expect(fetchContext.projectRouting).toEqual('ALL');
subscription.unsubscribe();
});

test('should emit once on parent project routing change', async () => {
const subscription = fetch$(api).pipe(skip(1)).subscribe(onFetchMock);
await waitForSearchSession();
expect(onFetchMock).not.toHaveBeenCalled();

api.parentApi.projectRouting$.next('_alias:_origin');

await waitFor(() => {
expect(onFetchMock).toHaveBeenCalledTimes(1);
});
const fetchContext = onFetchMock.mock.calls[0][0];
expect(fetchContext.projectRouting).toEqual('_alias:_origin');
subscription.unsubscribe();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,16 @@ function getFetchContext$(api: unknown): Observable<Omit<FetchContext, 'isReload
observables.query = api.parentApi.query$;
}

if (apiHasParentApi(api) && apiPublishesProjectRouting(api.parentApi)) {
observables.projectRouting = api.parentApi.projectRouting$;
}
observables.projectRouting = combineLatest({
local: apiPublishesProjectRouting(api) ? api.projectRouting$ : of(undefined),
parent:
apiHasParentApi(api) && apiPublishesProjectRouting(api.parentApi)
? api.parentApi.projectRouting$
: of(undefined),
}).pipe(
map(({ local, parent }) => local ?? parent),
distinctUntilChanged()
);

observables.timeRange = combineLatest({
local: apiPublishesTimeRange(api) ? api.timeRange$ : of(undefined),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@

export const ACTION_CUSTOMIZE_PANEL = 'ACTION_CUSTOMIZE_PANEL';
export const CUSTOM_TIME_RANGE_BADGE = 'CUSTOM_TIME_RANGE_BADGE';
export const CPS_USAGE_OVERRIDES_BADGE = 'CPS_USAGE_OVERRIDES_BADGE';
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type {
Action,
ActionExecutionMeta,
FrequentCompatibilityChangeAction,
} from '@kbn/ui-actions-plugin/public';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import React, { useState } from 'react';
import { EuiPopover, EuiText, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';

import type { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { apiPublishesProjectRouting, apiHasParentApi } from '@kbn/presentation-publishing';
import { combineLatest, map } from 'rxjs';
import { i18n } from '@kbn/i18n';
import { CPS_USAGE_OVERRIDES_BADGE } from './constants';
import { uiActions, core } from '../../kibana_services';
import { ACTION_EDIT_PANEL } from '../edit_panel_action/constants';
import { CONTEXT_MENU_TRIGGER } from '../triggers';

export class CpsUsageOverridesBadge
implements Action<EmbeddableApiContext>, FrequentCompatibilityChangeAction<EmbeddableApiContext>
{
public readonly type = CPS_USAGE_OVERRIDES_BADGE;
public readonly id = CPS_USAGE_OVERRIDES_BADGE;
public order = 8;

public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!apiPublishesProjectRouting(embeddable)) {
throw new IncompatibleActionError();
}

const overrideValue = embeddable.projectRouting$.value;

return i18n.translate('presentationPanel.badge.cpsUsageOverrides.displayName', {
defaultMessage: 'This panel overrides the dashboard CPS scope with: {value}',
values: {
value: overrideValue,
},
});
}

public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => {
const { embeddable } = context;
const [isPopoverOpen, setIsPopoverOpen] = useState(false);

if (!this.hasOverride(embeddable)) throw new IncompatibleActionError();

if (!apiPublishesProjectRouting(embeddable)) {
throw new IncompatibleActionError();
}

const overrideValue = embeddable.projectRouting$.value;
const badgeLabel = i18n.translate('presentationPanel.badge.cpsUsageOverrides.label', {
defaultMessage: 'CPS overrides',
});

const handleEditClick = async () => {
setIsPopoverOpen(false);
try {
const action = await uiActions.getAction(ACTION_EDIT_PANEL);
if (action) {
await action.execute({
...context,
trigger: { id: CONTEXT_MENU_TRIGGER },
});
}
} catch (error) {
core.notifications.toasts.addError(error, {
title: i18n.translate('presentationPanel.badge.cpsUsageOverrides.editError', {
defaultMessage: 'Failed to open panel configuration',
}),
});
}
};

return (
<EuiPopover
button={<button onClick={() => setIsPopoverOpen(!isPopoverOpen)}>{badgeLabel}</button>}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
anchorPosition="downCenter"
panelStyle={{ minWidth: 250 }}
>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText size="s">
<strong>
{i18n.translate('presentationPanel.badge.cpsUsageOverrides.popover.title', {
defaultMessage: 'CPS Override',
})}
</strong>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handleEditClick}
size="s"
data-test-subj="cpsUsageOverridesEditButton"
flush="right"
>
{i18n.translate('presentationPanel.badge.cpsUsageOverrides.popover.editButton', {
defaultMessage: 'Edit',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiText size="s">
{overrideValue === 'ALL'
? i18n.translate('presentationPanel.badge.cpsUsageOverrides.popover.allProjects', {
defaultMessage: 'All projects',
})
: overrideValue}
</EuiText>
</EuiPopover>
);
};

public couldBecomeCompatible({ embeddable }: EmbeddableApiContext) {
return (
apiPublishesProjectRouting(embeddable) &&
apiHasParentApi(embeddable) &&
apiPublishesProjectRouting(embeddable.parentApi)
);
}

public getCompatibilityChangesSubject({ embeddable }: EmbeddableApiContext) {
if (
apiPublishesProjectRouting(embeddable) &&
apiHasParentApi(embeddable) &&
apiPublishesProjectRouting(embeddable.parentApi)
) {
return combineLatest([embeddable.projectRouting$, embeddable.parentApi.projectRouting$]).pipe(
map(() => undefined)
);
}
return undefined;
}

public async execute(_context: ActionExecutionMeta & EmbeddableApiContext) {
// Badge is informational only - clicking shows tooltip but doesn't navigate
return;
}

public getIconType() {
return 'beaker';
}

public async isCompatible({ embeddable }: EmbeddableApiContext) {
return this.hasOverride(embeddable);
}

private hasOverride(embeddable: unknown): boolean {
if (!apiPublishesProjectRouting(embeddable)) {
return false;
}
return embeddable.projectRouting$.value !== undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@

export * from './customize_panel_action';
export * from './custom_time_range_badge';
export * from './cps_usage_overrides_badge';
Loading