Skip to content

Commit

Permalink
[Kibana] New "Saved Query Management" privilege to allow saving queri…
Browse files Browse the repository at this point in the history
…es across Kibana (#166937)

- Resolves #158173

Based on PoC #166260

## Summary

This PR adds a new "Saved Query Management" privilege with 2 options:
- `All` will override any per app privilege and will allow users to save
queries from any Kibana page
- `None` will default to per app privileges (backward-compatible option)

<img width="600" alt="Screenshot 2023-09-21 at 15 26 25"
src="https://github.com/elastic/kibana/assets/1415710/6d53548e-5c5a-4d6d-a86a-1e639cb77202">

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Matthias Wilhelm <matthias.wilhelm@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
  • Loading branch information
4 people authored Sep 29, 2023
1 parent d0a0a1f commit 7fa04e9
Show file tree
Hide file tree
Showing 40 changed files with 798 additions and 183 deletions.
1 change: 1 addition & 0 deletions .buildkite/ftr_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ enabled:
- x-pack/test/functional/apps/reporting_management/config.ts
- x-pack/test/functional/apps/rollup_job/config.ts
- x-pack/test/functional/apps/saved_objects_management/config.ts
- x-pack/test/functional/apps/saved_query_management/config.ts
- x-pack/test/functional/apps/security/config.ts
- x-pack/test/functional/apps/snapshot_restore/config.ts
- x-pack/test/functional/apps/spaces/config.ts
Expand Down
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,7 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations
/x-pack/test/examples/search_examples @elastic/kibana-data-discovery
/x-pack/test/functional/apps/data_views @elastic/kibana-data-discovery
/x-pack/test/functional/apps/discover @elastic/kibana-data-discovery
/x-pack/test/functional/apps/saved_query_management @elastic/kibana-data-discovery
/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover @elastic/kibana-data-discovery
/x-pack/test/search_sessions_integration @elastic/kibana-data-discovery
/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @elastic/kibana-data-discovery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const App = ({
showSearchBar={true}
indexPatterns={[dataView]}
useDefaultBehaviors={true}
showSaveQuery={true}
saveQueryMenuVisibility="allowed_by_app_privilege" // allowed only for this example app, use `globally_managed` by default
/>
<EuiPageTemplate.Section>
<EuiText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
navigation: { TopNavMenu },
embeddable: { getStateTransfer },
initializerContext: { allowByValueEmbeddables },
dashboardCapabilities: { saveQuery: showSaveQuery, showWriteControls },
dashboardCapabilities: { saveQuery: allowSaveQuery, showWriteControls },
} = pluginServices.getServices();
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
Expand Down Expand Up @@ -298,7 +298,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
useDefaultBehaviors={true}
savedQueryId={savedQueryId}
indexPatterns={allDataViews}
showSaveQuery={showSaveQuery}
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
appName={LEGACY_DASHBOARD_APP_ID}
visible={viewMode !== ViewMode.PRINT}
setMenuMountPoint={embedSettings || fullScreenMode ? undefined : setHeaderActionMenu}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('ContextApp test', () => {
showSearchBar: true,
showQueryInput: false,
showFilterBar: true,
showSaveQuery: false,
saveQueryMenuVisibility: 'hidden' as const,
showDatePicker: false,
indexPatterns: [dataViewMock],
useDefaultBehaviors: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
showSearchBar: true,
showQueryInput: false,
showFilterBar: true,
showSaveQuery: false,
saveQueryMenuVisibility: 'hidden' as const,
showDatePicker: false,
indexPatterns: [dataView],
useDefaultBehaviors: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,20 @@ jest.mock('../../../../customizations', () => ({
useDiscoverCustomization: jest.fn(),
}));

function getProps(savePermissions = true): DiscoverTopNavProps {
mockDiscoverService.capabilities.discover!.save = savePermissions;
const mockDefaultCapabilities = {
discover: { save: true },
} as unknown as typeof mockDiscoverService.capabilities;

function getProps(
{
capabilities,
}: {
capabilities?: Partial<typeof mockDiscoverService.capabilities>;
} = { capabilities: mockDefaultCapabilities }
): DiscoverTopNavProps {
if (capabilities) {
mockDiscoverService.capabilities = capabilities as typeof mockDiscoverService.capabilities;
}
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.internalState.transitions.setDataView(dataViewMock);

Expand Down Expand Up @@ -93,7 +105,7 @@ describe('Discover topnav component', () => {
});

test('generated config of TopNavMenu config is correct when discover save permissions are assigned', () => {
const props = getProps(true);
const props = getProps({ capabilities: { discover: { save: true } } });
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
Expand All @@ -105,7 +117,7 @@ describe('Discover topnav component', () => {
});

test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => {
const props = getProps(false);
const props = getProps({ capabilities: { discover: { save: false } } });
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
Expand All @@ -116,6 +128,32 @@ describe('Discover topnav component', () => {
expect(topMenuConfig).toEqual(['new', 'open', 'share', 'inspect']);
});

test('top nav is correct when discover saveQuery permission is granted', () => {
const props = getProps({ capabilities: { discover: { saveQuery: true } } });
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
</DiscoverMainProvider>
);
const statefulSearchBar = component.find(
mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu
);
expect(statefulSearchBar.props().saveQueryMenuVisibility).toBe('allowed_by_app_privilege');
});

test('top nav is correct when discover saveQuery permission is not granted', () => {
const props = getProps({ capabilities: { discover: { saveQuery: false } } });
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
</DiscoverMainProvider>
);
const statefulSearchBar = component.find(
mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu
);
expect(statefulSearchBar.props().saveQueryMenuVisibility).toBe('globally_managed');
});

describe('top nav customization', () => {
it('should call getMenuItems', () => {
mockUseCustomizations = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ export const DiscoverTopNav = ({
savedQueryId={savedQuery}
screenTitle={savedSearch.title}
showDatePicker={showDatePicker}
showSaveQuery={!isPlainRecord && Boolean(services.capabilities.discover.saveQuery)}
saveQueryMenuVisibility={
services.capabilities.discover.saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
}
showSearchBar={true}
useDefaultBehaviors={true}
dataViewPickerOverride={
Expand Down
67 changes: 34 additions & 33 deletions src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import classNames from 'classnames';
import { MountPoint } from '@kbn/core/public';
import { MountPointPortal } from '@kbn/kibana-react-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { StatefulSearchBarProps, SearchBarProps } from '@kbn/unified-search-plugin/public';
import { StatefulSearchBarProps } from '@kbn/unified-search-plugin/public';
import { AggregateQuery, Query } from '@kbn/es-query';
import { TopNavMenuData } from './top_nav_menu_data';
import { TopNavMenuItem } from './top_nav_menu_item';
Expand All @@ -30,38 +30,39 @@ type Badge = EuiBadgeProps & {
toolTipProps?: Partial<EuiToolTipProps>;
};

export type TopNavMenuProps<QT extends Query | AggregateQuery = Query> =
StatefulSearchBarProps<QT> &
Omit<SearchBarProps<QT>, 'kibana' | 'intl' | 'timeHistory'> & {
config?: TopNavMenuData[];
badges?: Badge[];
showSearchBar?: boolean;
showQueryInput?: boolean;
showDatePicker?: boolean;
showFilterBar?: boolean;
unifiedSearch?: UnifiedSearchPublicPluginStart;
className?: string;
visible?: boolean;
/**
* If provided, the menu part of the component will be rendered as a portal inside the given mount point.
*
* This is meant to be used with the `setHeaderActionMenu` core API.
*
* @example
* ```ts
* export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => {
* const topNavConfig = ...; // TopNavMenuProps
* return (
* <Router history=history>
* <TopNavMenu {...topNavConfig} setMenuMountPoint={setHeaderActionMenu}>
* <MyRoutes />
* </Router>
* )
* }
* ```
*/
setMenuMountPoint?: (menuMount: MountPoint | undefined) => void;
};
export type TopNavMenuProps<QT extends Query | AggregateQuery = Query> = Omit<
StatefulSearchBarProps<QT>,
'kibana' | 'intl' | 'timeHistory'
> & {
config?: TopNavMenuData[];
badges?: Badge[];
showSearchBar?: boolean;
showQueryInput?: boolean;
showDatePicker?: boolean;
showFilterBar?: boolean;
unifiedSearch?: UnifiedSearchPublicPluginStart;
className?: string;
visible?: boolean;
/**
* If provided, the menu part of the component will be rendered as a portal inside the given mount point.
*
* This is meant to be used with the `setHeaderActionMenu` core API.
*
* @example
* ```ts
* export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => {
* const topNavConfig = ...; // TopNavMenuProps
* return (
* <Router history=history>
* <TopNavMenu {...topNavConfig} setMenuMountPoint={setHeaderActionMenu}>
* <MyRoutes />
* </Router>
* )
* }
* ```
*/
setMenuMountPoint?: (menuMount: MountPoint | undefined) => void;
};

/*
* Top Nav Menu is a convenience wrapper component for:
Expand Down
Loading

0 comments on commit 7fa04e9

Please sign in to comment.