[Saved Queries] Rework saved query privileges (#202863)

## Summary

This PR reworks saved query privileges to rely solely on a single global
`savedQueryManagement` privilege, and eliminates app-specific overrides.
This change simplifies the security model for users, fixes bugginess in
the saved query management UI, and reduces code complexity associated
with maintaining two separate security mechanisms (app-specific
overrides and global saved query management privileges).

### Background

Saved queries allow users to store a combination of KQL or Lucene
queries, filters, and time filters to use across various applications in
Kibana. Access to saved query saved objects are currently granted by the
following feature privileges:
```json
[
  "feature_discover.all",
  "feature_dashboard.all",
  "feature_savedQueryManagement.all",
  "feature_maps.all",
  "feature_savedObjectsManagement.all",
  "feature_visualize.all"
]
```

There is also a saved query management UI within the Unified Search bar
shared by applications across Kibana:
<img
src="https://github.com/user-attachments/assets/e4a7539b-3dd4-4d47-9ff8-205281ef50e3"
width="500" />

The way access to this UI is managed in Kibana is currently confusing
and buggy:
- If a user has `feature_discover.all` and `feature_dashboard.all` they
will be able to load and save queries in Discover and Dashboard.
- If a user has `feature_discover.all` and `feature_dashboard.read` they
will be able to load queries in both Discover and Dashboard, but only
save queries in Discover (even though they have write access to the SO,
and API access). Instead they have to navigate to Discover to save a
query before navigating back to Dashboard to load it, making for a
confusing and frustrating UX.
- Access to the UI is even more confusing in apps not listed in the
above feature privileges (e.g. alerting, SLOs). Some of them chose to
check one of the above feature privileges, meaning users who otherwise
should have saved query access won't see the management UI if they don't
also have the exact feature privilege being checked. Other apps just
always show the management UI, leading to bugs and failures when users
without one of the above feature privileges attempt to save queries.

### Existing improvements

In v8.11.0, we introduced a new ["Saved Query
Management"](https://github.com/elastic/kibana/pull/166937) privilege,
allowing users to access saved queries across all of Kibana with a
single global privilege:
<img
src="https://github.com/user-attachments/assets/ccbe79a4-bd0b-4ed6-89c9-117cc1f99ee2"
width="600" />


When this privilege is added to a role, it solves the
`feature_discover.all` and `feature_dashboard.read` issue mentioned
above. However, it does not fix any of the mentioned issues for roles
without the new privilege. We have so far postponed further improvements
to avoid a breaking change.

### Approach

To fully resolve these issues and migrate to a single global privilege,
these changes have been made:
- Remove saved query SO access from all application feature privileges
and instead only allow access through the global saved query management
privilege.
- Stop relying on application feature privileges for toggling the saved
query management UI, and instead rely on the global privilege.

To implement this with minimal breaking changes, we've used the Kibana
privilege migration framework. This allows us to seamlessly migrate
existing roles containing feature privileges that currently provide
access to saved queries, ensuring they are assigned the global saved
query management privilege on upgrade.

As a result, we had to deprecate the following feature privileges,
replacing them with V2 privileges without saved query SO access:
```json
[
  "feature_discover.all",
  "feature_dashboard.all",
  "feature_maps.all",
  "feature_visualize.all"
]
```

Each area of code that currently relies on any of these feature
privileges had to be updated to instead access `feature_X_V2` instead
(as well as future code).

This PR still introduces a minor breaking change, since users who have
`feature_discover.all` and `feature_dashboard.read` are now able to save
queries in Dashboard after upgrade, but we believe this is a better UX
(and likely the expected one) and worth a small breaking change.

### Testing
- All existing privileges should continue to work as they do now,
including deprecated V1 feature privileges and customized serverless
privileges. There should be no changes for existing user roles apart
from the minor breaking change outlined above.
- Check that code changes in your area don't introduce breaking changes
to existing behaviour. Many of the changes are just updating client UI
capabilities code from `feature.privilege` to `feature_v2.privilege`,
which is backward compatible.
- The `savedQueryManagement` feature should now globally control access
to saved query management in Unified Search for all new user roles.
Regardless of privileges for Discover, Dashboard, Maps, or Visualize,
new user roles should follow this behaviour:
- If `savedQueryManagement` is `none`, the user cannot see or access the
saved query management UI or APIs.
- If `savedQueryManagement` is `read`, the user can load queries from
the UI and access read APIs, but cannot save queries from the UI or make
changes to queries through APIs.
- If `savedQueryManagement` is `all`, the user can both load and save
queries from the UI and through APIs.

### 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]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

This PR risks introducing unintended breaking changes to user privileges
related to saved queries if the deprecated features have not been
properly migrated, and users could gain or lose access to saved query
management on upgrade. This would be bad if it happened, but not overly
severe since it wouldn't grant them access to any ES data they couldn't
previously access (only query saved objects). We have automated testing
in place to help ensure features have been migrated correctly, but the
scope of these changes are broad and touch many places in the codebase.

Additionally, the UI capabilities types are not very strict, and are
referenced with string paths in many places, which makes changing them
riskier than changing strictly typed code. A combination of regex
searches and temporarily modifying the `Capabilities` type to cause type
errors for deprecated privileges was used to identify references in
code. Reviewers should consider if there are any other ways that UI
capabilities can be referenced which were not addressed in this PR.

Our automated tests already help mitigate the risk, but it's important
that code owners thoroughly review the changes in their area and
consider if they could have unintended consequences. The Platform
Security team should also review this PR thoroughly, especially since
some changes were made to platform code around privilege handling. The
Data Discovery team will also manually test the behaviour when upgrading
existing user roles with deprecated feature privileges as part of 9.0
upgrade testing.

---------

Co-authored-by: Matthias Wilhelm <matthias.wilhelm@elastic.co>
Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: “jeramysoucy” <jeramy.soucy@elastic.co>
This commit is contained in:
Davis McPhee 2025-01-29 17:34:58 -04:00 committed by GitHub
parent f3da71672c
commit b53d3990a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
187 changed files with 8466 additions and 1319 deletions

View file

@ -48,11 +48,11 @@ xpack.fleet.internal.registry.excludePackages: [
## Fine-tune the search solution feature privileges. Also, refer to `serverless.yml` for the project-agnostic overrides.
xpack.features.overrides:
### Dashboards feature is moved from Analytics category to the Search one.
dashboard.category: "enterpriseSearch"
dashboard_v2.category: "enterpriseSearch"
### Dev Tools feature is moved from Analytics category to the Search one.
dev_tools.category: "enterpriseSearch"
### Discover feature is moved from Analytics category to the Search one.
discover.category: "enterpriseSearch"
discover_v2.category: "enterpriseSearch"
### Machine Learning feature is moved from Analytics category to the Management one.
ml.category: "management"
### Stack Alerts feature is moved from Analytics category to the Search one renamed to simply `Alerts`.
@ -130,4 +130,4 @@ xpack.observabilityAIAssistant.scope: "search"
aiAssistantManagementSelection.preferredAIAssistantType: "observability"
xpack.observabilityAiAssistantManagement.logSourcesEnabled: false
xpack.observabilityAiAssistantManagement.spacesEnabled: false
xpack.observabilityAiAssistantManagement.visibilityEnabled: false
xpack.observabilityAiAssistantManagement.visibilityEnabled: false

View file

@ -32,10 +32,20 @@ xpack.features.overrides:
- feature: "observability"
privileges: [ "read" ]
### Dashboards feature should be moved from Analytics category to the Observability one.
dashboard.category: "observability"
dashboard_v2.category: "observability"
### Discover feature should be moved from Analytics category to the Observability one and its privileges are
### fine-tuned to grant access to Observability app.
discover:
privileges:
# Discover `All` feature privilege should implicitly grant `All` access to Observability app.
all.composedOf:
- feature: "observability"
privileges: [ "all" ]
# Discover `Read` feature privilege should implicitly grant `Read` access to Observability app.
read.composedOf:
- feature: "observability"
privileges: [ "read" ]
discover_v2:
category: "observability"
privileges:
# Discover `All` feature privilege should implicitly grant `All` access to Observability app.
@ -226,4 +236,4 @@ xpack.ml.compatibleModuleType: 'observability'
console.ui.embeddedEnabled: false
# Disable role management (custom roles)
xpack.security.roleManagementEnabled: false
xpack.security.roleManagementEnabled: false

View file

@ -15,10 +15,15 @@ xpack.searchIndices.enabled: false
## Fine-tune the security solution feature privileges. Also, refer to `serverless.yml` for the project-agnostic overrides.
xpack.features.overrides:
### Dashboard feature is hidden in Role management since it's automatically granted by SIEM feature.
dashboard.hidden: true
### Discover feature is hidden in Role management since it's automatically granted by SIEM feature.
### The following features are hidden in Role management since they're automatically granted by SIEM feature.
discover.hidden: true
discover_v2.hidden: true
dashboard.hidden: true
dashboard_v2.hidden: true
visualize.hidden: true
visualize_v2.hidden: true
maps.hidden: true
maps_v2.hidden: true
### Machine Learning feature is moved from Analytics category to the Security one as the last item.
ml:
category: "security"
@ -29,25 +34,29 @@ xpack.features.overrides:
### Security's `All` feature privilege should implicitly grant `All` access to Discover, Dashboard, Maps, and
### Visualize features.
all.composedOf:
- feature: "discover"
- feature: "discover_v2"
privileges: [ "all" ]
- feature: "dashboard"
- feature: "dashboard_v2"
privileges: [ "all" ]
- feature: "visualize"
- feature: "visualize_v2"
privileges: [ "all" ]
- feature: "maps"
- feature: "maps_v2"
privileges: [ "all" ]
- feature: "savedQueryManagement"
privileges: [ "all" ]
# Security's `Read` feature privilege should implicitly grant `Read` access to Discover, Dashboard, Maps, and
# Visualize features. Additionally, it should implicitly grant privilege to create short URLs in Discover,
### Dashboard, and Visualize apps.
read.composedOf:
- feature: "discover"
- feature: "discover_v2"
privileges: [ "read" ]
- feature: "dashboard"
- feature: "dashboard_v2"
privileges: [ "read" ]
- feature: "visualize"
- feature: "visualize_v2"
privileges: [ "read" ]
- feature: "maps"
- feature: "maps_v2"
privileges: [ "read" ]
- feature: "savedQueryManagement"
privileges: [ "read" ]
### Security's feature privileges are fine-tuned to grant access to Discover, Dashboard, Maps, and Visualize apps.

View file

@ -36,6 +36,30 @@ xpack.features.overrides:
url_create:
disabled: true
includeIn: "read"
dashboard_v2:
privileges:
### Dashboard's `All` feature privilege should implicitly grant `All` access to Maps and Visualize features.
all.composedOf:
- feature: "maps_v2"
privileges: [ "all" ]
- feature: "visualize_v2"
privileges: [ "all" ]
### Dashboard's `Read` feature privilege should implicitly grant `Read` access to Maps and Visualize features.
### Additionally, it should implicitly grant privilege to create short URLs in Visualize app.
read.composedOf:
- feature: "maps_v2"
privileges: [ "read" ]
- feature: "visualize_v2"
privileges: [ "read" ]
### All Dashboard sub-feature privileges should be hidden: reporting capabilities will be granted via dedicated
### Reporting feature and short URL sub-feature privilege should be granted for both `All` and `Read`.
subFeatures.privileges:
download_csv_report.disabled: true
generate_report.disabled: true
store_search_session.disabled: true
url_create:
disabled: true
includeIn: "read"
discover:
### All Discover sub-feature privileges should be hidden: reporting capabilities will be granted via dedicated
### Reporting feature and short URL sub-feature privilege should be granted for both `All` and `Read`.
@ -45,20 +69,32 @@ xpack.features.overrides:
url_create:
disabled: true
includeIn: "read"
discover_v2:
### All Discover sub-feature privileges should be hidden: reporting capabilities will be granted via dedicated
### Reporting feature and short URL sub-feature privilege should be granted for both `All` and `Read`.
subFeatures.privileges:
generate_report.disabled: true
store_search_session.disabled: true
url_create:
disabled: true
includeIn: "read"
### Shared images feature is hidden in Role management since it's not needed.
filesSharedImage.hidden: true
### Maps feature is hidden in Role management since it's automatically granted by Dashboard feature.
maps.hidden: true
maps_v2.hidden: true
### Reporting feature is supposed to give access to reporting capabilities across different features.
reporting:
privileges:
all.composedOf:
- feature: "dashboard"
- feature: "dashboard_v2"
privileges: [ "download_csv_report" ]
- feature: "discover"
- feature: "discover_v2"
privileges: [ "generate_report" ]
### Visualize feature is hidden in Role management since it's automatically granted by Dashboard feature.
visualize:
### The short URL sub-feature privilege should be always granted.
subFeatures.privileges.url_create.includeIn: "read"
visualize_v2:
hidden: true
### The short URL sub-feature privilege should be always granted.
subFeatures.privileges.url_create.includeIn: "read"
@ -236,4 +272,4 @@ xpack.dataUsage.enabled: true
xpack.dataUsage.enableExperimental: ['dataUsageDisabled']
# Ensure Serverless is using the Amsterdam theme
uiSettings.experimental.defaultTheme: "amsterdam"
uiSettings.experimental.defaultTheme: "amsterdam"

View file

@ -17,9 +17,9 @@ which include the *Discover* configuration&mdash;selected columns in the documen
Discover sessions are primarily used for adding search results to a dashboard.
[role="xpack"]
==== Read-only access
If you have insufficient privileges to save queries,
the *Save* button isn't visible in the saved query management popover.
==== Saved query access
If you have insufficient privileges to manage saved queries,
you will be unable to load or save queries from the saved query management popover.
For more information, see <<xpack-security-authorization, Granting access to Kibana>>
==== Save a query

View file

@ -110,11 +110,11 @@ PUT <kibana host>:<port>/api/security/role/custom_reporting_user
"spaces": ["*"],
"base": [],
"feature": {
"dashboard": ["generate_report", <1>
"dashboard_v2": ["generate_report", <1>
"download_csv_report"], <2>
"discover": ["generate_report"], <3>
"discover_v2": ["generate_report"], <3>
"canvas": ["generate_report"], <4>
"visualize": ["generate_report"] <5>
"visualize_v2": ["generate_report"] <5>
}
}]
}
@ -147,8 +147,8 @@ PUT localhost:5601/api/security/role/custom_reporting_user
{
"base": [],
"feature": {
"dashboard": [ "all" ], <1>
"discover": [ "all" ], <2>
"dashboard_v2": [ "all" ], <1>
"discover_v2": [ "all" ], <2>
},
"spaces": [ "*" ]
}

View file

@ -271,6 +271,23 @@ Use Kibana feature privileges to control access to reporting features. For more
- The `xpack.reporting.roles.allow` setting is no longer supported. If you have a `xpack.reporting.roles.allow` value in your `kibana.yml`, you should remove this setting and assign privileges to reporting features using Kibana feature privileges.
====
[discrete]
[[breaking-202863]]
.Saved query privileges have been reworked (9.0.0)
[%collapsible]
====
*Details* +
Saved query privileges have been reworked to rely solely on a single global `savedQueryManagement` privilege, and eliminate app-specific overrides (e.g. implicit access with `all` privilege for Discover, Dashboard, Maps, and Visualize apps). This change simplifies the security model and ensures consistency in the saved query management UI across Kibana, but results in different handling of saved query privileges for new user roles, and minor breaking changes to the existing management UX.
For more information, refer to {kibana-pull}202863[#202863].
*Impact* +
The `savedQueryManagement` feature privilege now globally controls access to saved query management for all new user roles. Regardless of privileges for Discover, Dashboard, Maps, or Visualize, new user roles follow this behaviour:
. If `savedQueryManagement` is `none`, the user cannot see or access the saved query management UI or APIs.
. If `savedQueryManagement` is `read`, the user can load queries from the UI and access read APIs, but cannot save queries from the UI or make changes to queries through APIs.
. If `savedQueryManagement` is `all`, the user can both load and save queries from the UI and through APIs.
*Action* +
Existing user roles that were previously implicitly granted access to saved queries through the dashboard, discover, visualize, or maps feature privileges will retain that access to prevent breaking changes. While no action is required for existing roles, its still advisable to audit relevant roles and re-save them to migrate to the latest privileges model. For new roles, ensure that the savedQueryManagement privilege is set as needed.
====
[float]
=== Deprecation notices

View file

@ -65,8 +65,8 @@ PUT /api/security/role/my_kibana_role
{
"base": [],
"feature": {
"visualize": ["all"],
"dashboard": ["read", "url_create"]
"visualize_v2": ["all"],
"dashboard_v2": ["read", "url_create"]
},
"spaces": ["marketing"]
}

View file

@ -98,7 +98,7 @@ export const App = ({
showSearchBar={true}
indexPatterns={[dataView]}
useDefaultBehaviors={true}
saveQueryMenuVisibility="allowed_by_app_privilege" // allowed only for this example app, use `globally_managed` by default
allowSavingQueries
/>
<EuiPageTemplate.Section>
<EuiText>

View file

@ -54,10 +54,11 @@ viewer:
- feature_actions.read
- feature_builtInAlerts.read
- feature_osquery.read
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
- feature_dataQuality.all
resources: '*'
run_as: []
@ -137,10 +138,11 @@ editor:
- feature_actions.endpoint_security_execute
- feature_builtInAlerts.all
- feature_osquery.all
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
run_as: []
@ -191,10 +193,11 @@ t1_analyst:
- feature_builtInAlerts.read
- feature_osquery.read
- feature_osquery.run_saved_queries
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
t2_analyst:
@ -248,10 +251,11 @@ t2_analyst:
- feature_builtInAlerts.read
- feature_osquery.read
- feature_osquery.run_saved_queries
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
t3_analyst:
@ -322,10 +326,11 @@ t3_analyst:
- feature_actions.endpoint_security_execute
- feature_builtInAlerts.all
- feature_osquery.all
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
threat_intelligence_analyst:
@ -386,10 +391,11 @@ threat_intelligence_analyst:
- feature_actions.read
- feature_builtInAlerts.read
- feature_osquery.all
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
rule_author:
@ -457,10 +463,11 @@ rule_author:
- feature_actions.read
- feature_builtInAlerts.all
- feature_osquery.all
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
soc_manager:
@ -535,10 +542,11 @@ soc_manager:
- feature_builtInAlerts.all
- feature_osquery.all
- feature_indexPatterns.all
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
detections_admin:
@ -596,10 +604,11 @@ detections_admin:
- feature_actions.all
- feature_builtInAlerts.all
- feature_dev_tools.all
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
platform_engineer:
@ -661,10 +670,11 @@ platform_engineer:
- feature_fleetv2.all
- feature_osquery.all
- feature_indexPatterns.all
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
endpoint_operations_analyst:
@ -737,10 +747,11 @@ endpoint_operations_analyst:
- feature_osquery.all
- feature_fleet.all
- feature_fleetv2.all
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
endpoint_policy_manager:
@ -815,10 +826,11 @@ endpoint_policy_manager:
- feature_osquery.all
- feature_fleet.all
- feature_fleetv2.all
- feature_discover.all
- feature_dashboard.all
- feature_maps.all
- feature_visualize.all
- feature_discover_v2.all
- feature_dashboard_v2.all
- feature_maps_v2.all
- feature_visualize_v2.all
- feature_savedQueryManagement.all
resources: '*'
# admin role defined in elasticsearch controller

View file

@ -126,7 +126,8 @@
{
"names": [
"metrics-endpoint.metadata_current_*",
".fleet-agents*", ".fleet-actions*",
".fleet-agents*",
".fleet-actions*",
"risk-score.risk-score-*",
".entities.v1.latest.security_*"
],
@ -162,10 +163,11 @@
"actions": ["read"],
"builtInAlerts": ["all"],
"osquery": ["all"],
"discover": ["all"],
"dashboard": ["all"],
"maps": ["all"],
"visualize": ["all"]
"discover_v2": ["all"],
"dashboard_v2": ["all"],
"maps_v2": ["all"],
"visualize_v2": ["all"],
"savedQueryManagement": ["all"]
},
"spaces": ["*"],
"base": []

View file

@ -14,7 +14,7 @@
*
* @public
*/
export interface Capabilities {
export type Capabilities = {
/** Navigation link capabilities. */
navLinks: Record<string, boolean>;
@ -28,4 +28,11 @@ export interface Capabilities {
/** Custom capabilities, registered by plugins. */
[key: string]: Record<string, boolean | Record<string, boolean>>;
}
} & {
// These capabilities have been replaced by discover_v2, dashboard_v2 etc.
// The purpose of these types is to avoid anyone accidentally depending on the removed capabilities.
discover?: {};
dashboard?: {};
maps?: {};
visualize?: {};
};

View file

@ -71,7 +71,7 @@ describe('GetCsvReportPanelAction', () => {
mockStartServicesPayload = [
{
...core,
application: { capabilities: { dashboard: { downloadCsv: true } } },
application: { capabilities: { dashboard_v2: { downloadCsv: true } } },
} as unknown as CoreStart,
{
data: dataPluginMock.createStartContract(),

View file

@ -137,7 +137,7 @@ export class ReportingCsvPanelAction implements ActionDefinition<EmbeddableApiCo
const licenseHasCsvReporting = checkLicense(license.check('reporting', 'basic')).showLinks;
// NOTE: For historical reasons capability identifier is called `downloadCsv. It can not be renamed.
const capabilityHasCsvReporting = application.capabilities.dashboard?.downloadCsv === true;
const capabilityHasCsvReporting = application.capabilities.dashboard_v2?.downloadCsv === true;
if (!licenseHasCsvReporting || !capabilityHasCsvReporting) {
return false;
}

View file

@ -75,7 +75,7 @@ export const reportingCsvShareProvider = ({
const licenseHasCsvReporting = licenseCheck.showLinks;
const licenseDisabled = !licenseCheck.enableLinks;
const capabilityHasCsvReporting = application.capabilities.discover?.generateCsv === true;
const capabilityHasCsvReporting = application.capabilities.discover_v2?.generateCsv === true;
const generateReportingJobCSV = ({ intl }: { intl: InjectedIntl }) => {
const decoratedJobParams = apiClient.getDecoratedJobParams(getJobParams());

View file

@ -64,9 +64,9 @@ export const reportingExportModalProvider = ({
const licenseDisabled = !enableLinks;
const capabilityHasDashboardScreenshotReporting =
application.capabilities.dashboard?.generateScreenshot === true;
application.capabilities.dashboard_v2?.generateScreenshot === true;
const capabilityHasVisualizeScreenshotReporting =
application.capabilities.visualize?.generateScreenshot === true;
application.capabilities.visualize_v2?.generateScreenshot === true;
if (!licenseHasScreenshotReporting) {
return [];

View file

@ -109,7 +109,7 @@ export const AlertsSearchBar = ({
onRefresh,
showDatePicker,
showQueryInput: true,
saveQueryMenuVisibility: 'allowed_by_app_privilege',
allowSavingQueries: true,
showSubmitButton,
submitOnBlur,
onQueryChange: onSearchQueryChange,

View file

@ -16,7 +16,7 @@ import { Capabilities } from '@kbn/core/types';
describe('useColumns', () => {
const defaultProps = {
capabilities: { discover: { save: true } } as unknown as Capabilities,
capabilities: { discover_v2: { save: true } } as unknown as Capabilities,
config: configMock,
dataView: dataViewMock,
dataViews: dataViewsMock,

View file

@ -35,7 +35,7 @@ export function setupSavedObjects(coreSetup: CoreSetup) {
getInAppUrl: (obj: { id: string }) => ({
// TODO link to specific object
path: `/app/${VISUALIZE_APP_NAME}#/${ANNOTATIONS_LISTING_VIEW_ID}`,
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
}),
},
migrations: () => {

View file

@ -50,7 +50,7 @@ export const getTableList = (
savedObjectsTagging={services.savedObjectsTagging}
uiSettings={services.core.uiSettings}
eventAnnotationService={services.eventAnnotationService}
visualizeCapabilities={services.core.application.capabilities.visualize}
visualizeCapabilities={services.core.application.capabilities.visualize_v2}
parentProps={parentProps}
dataViews={services.dataViews}
createDataView={services.createDataView}

View file

@ -14,7 +14,6 @@ import type { DashboardAttributes, DashboardPanel } from '../server/content_mana
export interface DashboardCapabilities {
showWriteControls: boolean;
saveQuery: boolean;
createNew: boolean;
show: boolean;
[key: string]: boolean;

View file

@ -16,7 +16,7 @@ import { showPublicUrlSwitch, ShowShareModal, ShowShareModalProps } from './show
import { getDashboardBackupService } from '../../../services/dashboard_backup_service';
describe('showPublicUrlSwitch', () => {
test('returns false if "dashboard" app is not available', () => {
test('returns false if "dashboard_v2" app is not available', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
@ -27,12 +27,12 @@ describe('showPublicUrlSwitch', () => {
expect(result).toBe(false);
});
test('returns false if "dashboard" app is not accessible', () => {
test('returns false if "dashboard_v2" app is not accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
dashboard: {
dashboard_v2: {
show: false,
},
};
@ -41,12 +41,12 @@ describe('showPublicUrlSwitch', () => {
expect(result).toBe(false);
});
test('returns true if "dashboard" app is not available an accessible', () => {
test('returns true if "dashboard_v2" app is not available an accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
dashboard: {
dashboard_v2: {
show: true,
},
};

View file

@ -41,9 +41,9 @@ export interface ShowShareModalProps {
}
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
if (!anonymousUserCapabilities.dashboard) return false;
if (!anonymousUserCapabilities.dashboard_v2) return false;
const dashboard = anonymousUserCapabilities.dashboard;
const dashboard = anonymousUserCapabilities.dashboard_v2;
return !!dashboard.show;
};

View file

@ -57,7 +57,7 @@ describe('DashboardEmptyScreen', () => {
});
test('renders correctly with readonly mode', () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = false;
const component = mountComponent('view');
expect(component.render()).toMatchSnapshot();
@ -72,7 +72,7 @@ describe('DashboardEmptyScreen', () => {
// even when in edit mode, readonly users should not have access to the editing buttons in the empty prompt.
test('renders correctly with readonly and edit mode', () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = false;
const component = mountComponent('edit');
expect(component.render()).toMatchSnapshot();

View file

@ -65,7 +65,7 @@ function mountWith({ props: incomingProps }: { props?: Partial<DashboardListingP
}
test('initial filter is passed through', async () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = false;
let component: ReactWrapper;
@ -80,7 +80,7 @@ test('initial filter is passed through', async () => {
});
test('when showWriteControls is true, table list view is passed editing functions', async () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = true;
let component: ReactWrapper;
@ -99,7 +99,7 @@ test('when showWriteControls is true, table list view is passed editing function
});
test('when showWriteControls is false, table list view is not passed editing functions', async () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = false;
let component: ReactWrapper;

View file

@ -56,7 +56,7 @@ function mountWith({
}
test('renders readonly empty prompt when showWriteControls is off', async () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = false;
let component: ReactWrapper;
await act(async () => {
@ -68,7 +68,7 @@ test('renders readonly empty prompt when showWriteControls is off', async () =>
});
test('renders empty prompt with link when showWriteControls is on', async () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = true;
let component: ReactWrapper;
await act(async () => {
@ -80,7 +80,7 @@ test('renders empty prompt with link when showWriteControls is on', async () =>
});
test('renders disabled action button when disableCreateDashboardButton is true', async () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = true;
let component: ReactWrapper;
await act(async () => {
@ -95,7 +95,7 @@ test('renders disabled action button when disableCreateDashboardButton is true',
});
test('renders continue button when no dashboards exist but one is in progress', async () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = true;
let component: ReactWrapper;
let props: DashboardListingEmptyPromptProps;
await act(async () => {
@ -114,7 +114,7 @@ test('renders continue button when no dashboards exist but one is in progress',
});
test('renders discard button when no dashboards exist but one is in progress', async () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = true;
let component: ReactWrapper;
await act(async () => {
({ component } = mountWith({

View file

@ -236,7 +236,7 @@ describe('useDashboardListingTable', () => {
test('createItem should be undefined when showWriteControls equals false', () => {
coreServices.application.capabilities = {
...coreServices.application.capabilities,
dashboard: {
dashboard_v2: {
showWriteControls: false,
},
};
@ -251,7 +251,7 @@ describe('useDashboardListingTable', () => {
});
test('deleteItems should be undefined when showWriteControls equals false', () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = false;
const { result } = renderHook(() =>
useDashboardListingTable({
@ -264,7 +264,7 @@ describe('useDashboardListingTable', () => {
});
test('editItem should be undefined when showWriteControls equals false', () => {
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard_v2.showWriteControls = false;
const { result } = renderHook(() =>
useDashboardListingTable({

View file

@ -339,9 +339,7 @@ export function InternalDashboardTopNav({
useDefaultBehaviors={true}
savedQueryId={savedQueryId}
indexPatterns={allDataViews ?? []}
saveQueryMenuVisibility={
getDashboardCapabilities().saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
}
allowSavingQueries
appName={LEGACY_DASHBOARD_APP_ID}
visible={viewMode !== 'print'}
setMenuMountPoint={

View file

@ -41,7 +41,6 @@ import { SearchDashboardsResponse } from './dashboard_content_management_service
const defaultDashboardCapabilities: DashboardCapabilities = {
show: true,
createNew: true,
saveQuery: true,
createShortUrl: true,
showWriteControls: true,
storeSearchSession: true,
@ -49,7 +48,7 @@ const defaultDashboardCapabilities: DashboardCapabilities = {
export const setStubKibanaServices = () => {
const core = coreMock.createStart();
(core.application.capabilities as any).dashboard = defaultDashboardCapabilities;
(core.application.capabilities as any).dashboard_v2 = defaultDashboardCapabilities;
setKibanaServices(core, {
contentManagement: contentManagementMock.createStartContract(),

View file

@ -13,13 +13,12 @@ import { coreServices } from '../services/kibana_services';
export const getDashboardCapabilities = (): DashboardCapabilities => {
const {
application: {
capabilities: { dashboard },
capabilities: { dashboard_v2: dashboard },
},
} = coreServices;
return {
show: Boolean(dashboard.show),
saveQuery: Boolean(dashboard.saveQuery),
createNew: Boolean(dashboard.createNew),
createShortUrl: Boolean(dashboard.createShortUrl),
showWriteControls: Boolean(dashboard.showWriteControls),

View file

@ -10,12 +10,11 @@
import { DashboardCapabilities } from '../common/types';
export const capabilitiesProvider = (): {
dashboard: DashboardCapabilities;
dashboard_v2: DashboardCapabilities;
} => ({
dashboard: {
dashboard_v2: {
createNew: true,
show: true,
showWriteControls: true,
saveQuery: true,
},
});

View file

@ -39,7 +39,7 @@ export const createDashboardSavedObjectType = ({
getInAppUrl(obj) {
return {
path: `/app/dashboards#/view/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'dashboard.show',
uiCapabilitiesPath: 'dashboard_v2.show',
};
},
},

View file

@ -32,7 +32,7 @@ export const querySavedObjectType: SavedObjectsType = {
getInAppUrl(obj) {
return {
path: `/app/discover#/?_a=(savedQuery:'${encodeURIComponent(obj.id)}')`,
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
};
},
},

View file

@ -174,10 +174,10 @@ export function createDiscoverServicesMock(): DiscoverServices {
docLinks: docLinksServiceMock.createStartContract(),
embeddable: embeddablePluginMock.createStartContract(),
capabilities: {
visualize: {
visualize_v2: {
show: true,
},
discover: {
discover_v2: {
save: false,
},
advancedSettings: {

View file

@ -15,7 +15,7 @@ const capabilities = deepFreeze({
catalogue: {},
management: {},
navLinks: {},
discover: {
discover_v2: {
show: true,
edit: false,
},

View file

@ -88,7 +88,6 @@ describe('ContextApp test', () => {
showSearchBar: true,
showQueryInput: false,
showFilterBar: true,
saveQueryMenuVisibility: 'hidden' as const,
showDatePicker: false,
indexPatterns: [dataViewMock],
useDefaultBehaviors: true,

View file

@ -230,7 +230,6 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
showSearchBar: true,
showQueryInput: false,
showFilterBar: true,
saveQueryMenuVisibility: 'hidden' as const,
showDatePicker: false,
indexPatterns: [dataView],
useDefaultBehaviors: true,

View file

@ -30,7 +30,7 @@ export const renderApp = ({
}: RenderAppProps) => {
const { history, capabilities, chrome, data, core } = services;
if (!capabilities.discover.save) {
if (!capabilities.discover_v2.save) {
chrome.setBadge({
text: i18n.translate('discover.badge.readOnly.text', {
defaultMessage: 'Read only',

View file

@ -100,7 +100,7 @@ export const getShareAppMenuItem = ({
services.share.toggleShareContextMenu({
anchorElement,
allowEmbed: false,
allowShortUrl: !!services.capabilities.discover.createShortUrl,
allowShortUrl: !!services.capabilities.discover_v2.createShortUrl,
shareableUrl,
shareableUrlForSavedObject,
shareableUrlLocatorParams: { locator, params },

View file

@ -55,7 +55,7 @@ jest.mock('../../../../customizations', () => ({
}));
const mockDefaultCapabilities = {
discover: { save: true },
discover_v2: { save: true },
} as unknown as typeof mockDiscoverService.capabilities;
function getProps(
@ -107,7 +107,7 @@ describe('Discover topnav component', () => {
});
test('generated config of TopNavMenu config is correct when discover save permissions are assigned', () => {
const props = getProps({ capabilities: { discover: { save: true } } });
const props = getProps({ capabilities: { discover_v2: { save: true } } });
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
@ -119,7 +119,7 @@ describe('Discover topnav component', () => {
});
test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => {
const props = getProps({ capabilities: { discover: { save: false } } });
const props = getProps({ capabilities: { discover_v2: { save: false } } });
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
@ -130,32 +130,6 @@ describe('Discover topnav component', () => {
expect(topMenuConfig).toEqual(['inspect', 'new', 'open', 'share']);
});
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 allow disabling default menu items', () => {
mockUseCustomizations = true;

View file

@ -235,9 +235,7 @@ export const DiscoverTopNav = ({
savedQueryId={savedQuery}
screenTitle={savedSearch.title}
showDatePicker={showDatePicker}
saveQueryMenuVisibility={
services.capabilities.discover.saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
}
allowSavingQueries
showSearchBar={true}
useDefaultBehaviors={true}
dataViewPickerOverride={

View file

@ -17,7 +17,7 @@ import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks';
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
const discoverServiceMock = createDiscoverServicesMock();
discoverServiceMock.capabilities.discover.save = true;
discoverServiceMock.capabilities.discover_v2.save = true;
describe('getTopNavBadges()', function () {
test('should not return the unsaved changes badge if no changes', () => {
@ -59,7 +59,7 @@ describe('getTopNavBadges()', function () {
test('should not show save in unsaved changed badge for read-only user', async () => {
const discoverServiceMockReadOnly = createDiscoverServicesMock();
discoverServiceMockReadOnly.capabilities.discover.save = false;
discoverServiceMockReadOnly.capabilities.discover_v2.save = false;
const topNavBadges = getTopNavBadges({
hasUnsavedChanges: true,
services: discoverServiceMockReadOnly,
@ -132,7 +132,7 @@ describe('getTopNavBadges()', function () {
describe('solutions view badge', () => {
const discoverServiceWithSpacesMock = createDiscoverServicesMock();
discoverServiceWithSpacesMock.capabilities.discover.save = true;
discoverServiceWithSpacesMock.capabilities.discover_v2.save = true;
discoverServiceWithSpacesMock.spaces = spacesPluginMock.createStartContract();
test('should return the solutions view badge when spaces is enabled', () => {

View file

@ -63,12 +63,12 @@ export const getTopNavBadges = ({
await stateContainer.actions.undoSavedSearchChanges();
},
onSave:
services.capabilities.discover.save && !isManaged
services.capabilities.discover_v2.save && !isManaged
? async () => {
await saveSearch();
}
: undefined,
onSaveAs: services.capabilities.discover.save
onSaveAs: services.capabilities.discover_v2.save
? async () => {
await saveSearch(true);
}

View file

@ -20,7 +20,7 @@ describe('useTopNavLinks', () => {
const services = {
...createDiscoverServicesMock(),
capabilities: {
discover: {
discover_v2: {
save: true,
},
},

View file

@ -185,7 +185,7 @@ export const useTopNavLinks = ({
entries.unshift(esqLDataViewTransitionToggle);
}
if (services.capabilities.discover.save && !defaultMenu?.saveItem?.disabled) {
if (services.capabilities.discover_v2.save && !defaultMenu?.saveItem?.disabled) {
const saveSearch = {
id: 'save',
label: i18n.translate('discover.localMenu.saveTitle', {

View file

@ -498,7 +498,7 @@ export function getDiscoverStateContainer({
}),
{
isDisabled: () =>
services.capabilities.discover.storeSearchSession
services.capabilities.discover_v2.storeSearchSession
? { disabled: false }
: {
disabled: true,

View file

@ -50,7 +50,7 @@ export class ViewSavedSearchAction implements Action<EmbeddableApiContext> {
async isCompatible({ embeddable }: EmbeddableApiContext) {
const { capabilities } = this.application;
const hasDiscoverPermissions =
(capabilities.discover.show as boolean) || (capabilities.discover.save as boolean);
(capabilities.discover_v2.show as boolean) || (capabilities.discover_v2.save as boolean);
if (!hasDiscoverPermissions) return false; // early return to delay async import until absolutely necessary
const { compatibilityCheck } = await import('./view_saved_search_compatibility_check');

View file

@ -156,7 +156,7 @@ export const getSearchEmbeddableFactory = ({
}),
canLinkToLibrary: async () => {
return (
discoverServices.capabilities.discover.save && !Boolean(savedObjectId$.getValue())
discoverServices.capabilities.discover_v2.save && !Boolean(savedObjectId$.getValue())
);
},
canUnlinkFromLibrary: async () => Boolean(savedObjectId$.getValue()),

View file

@ -372,7 +372,7 @@ export class DiscoverPlugin
const [coreStart, deps] = await core.getStartServices();
return {
executeTriggerActions: deps.uiActions.executeTriggerActions,
isEditable: () => coreStart.application.capabilities.discover.save as boolean,
isEditable: () => coreStart.application.capabilities.discover_v2.save as boolean,
};
};

View file

@ -362,7 +362,7 @@ describe('getSharingData', () => {
});
describe('showPublicUrlSwitch', () => {
test('returns false if "discover" app is not available', () => {
test('returns false if "discover_v2" app is not available', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
@ -373,12 +373,12 @@ describe('showPublicUrlSwitch', () => {
expect(result).toBe(false);
});
test('returns false if "discover" app is not accessible', () => {
test('returns false if "discover_v2" app is not accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
discover: {
discover_v2: {
show: false,
},
};
@ -387,12 +387,12 @@ describe('showPublicUrlSwitch', () => {
expect(result).toBe(false);
});
test('returns true if "discover" app is not available an accessible', () => {
test('returns true if "discover_v2" app is not available an accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
discover: {
discover_v2: {
show: true,
},
};

View file

@ -137,15 +137,14 @@ export async function getSharingData(
export interface DiscoverCapabilities {
createShortUrl?: boolean;
save?: boolean;
saveQuery?: boolean;
show?: boolean;
storeSearchSession?: boolean;
}
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
if (!anonymousUserCapabilities.discover) return false;
if (!anonymousUserCapabilities.discover_v2) return false;
const discover = anonymousUserCapabilities.discover as unknown as DiscoverCapabilities;
const discover = anonymousUserCapabilities.discover_v2 as unknown as DiscoverCapabilities;
return !!discover.show;
};

View file

@ -8,10 +8,9 @@
*/
export const capabilitiesProvider = () => ({
discover: {
discover_v2: {
show: true,
createShortUrl: true,
save: true,
saveQuery: true,
},
});

View file

@ -18,11 +18,11 @@ import { setKibanaServices } from './kibana_services';
const setDefaultPresentationUtilCapabilities = (core: CoreStart) => {
core.application.capabilities = {
...core.application.capabilities,
dashboard: {
dashboard_v2: {
show: true,
createNew: true,
},
visualize: {
visualize_v2: {
save: true,
},
advancedSettings: {

View file

@ -17,7 +17,11 @@ interface PresentationCapabilities {
}
export const getPresentationCapabilities = (): PresentationCapabilities => {
const { dashboard, visualize, advancedSettings } = coreServices.application.capabilities;
const {
dashboard_v2: dashboard,
visualize_v2: visualize,
advancedSettings,
} = coreServices.application.capabilities;
return {
canAccessDashboards: Boolean(dashboard.show),

View file

@ -282,7 +282,7 @@ exports[`SavedObjectsTable should render normally 1`] = `
"icon": "search",
"inAppUrl": Object {
"path": "/discover/2",
"uiCapabilitiesPath": "discover.show",
"uiCapabilitiesPath": "discover_v2.show",
},
"title": "MySearch",
},
@ -294,7 +294,7 @@ exports[`SavedObjectsTable should render normally 1`] = `
"icon": "dashboardApp",
"inAppUrl": Object {
"path": "/dashboard/3",
"uiCapabilitiesPath": "dashboard.show",
"uiCapabilitiesPath": "dashboard_v2.show",
},
"title": "MyDashboard",
},
@ -306,7 +306,7 @@ exports[`SavedObjectsTable should render normally 1`] = `
"icon": "visualizeApp",
"inAppUrl": Object {
"path": "/edit/4",
"uiCapabilitiesPath": "visualize.show",
"uiCapabilitiesPath": "visualize_v2.show",
},
"title": "MyViz",
},

View file

@ -88,7 +88,7 @@ exports[`Relationships should render dashboards normally 1`] = `
"icon": "visualizeApp",
"inAppUrl": Object {
"path": "/app/visualize#/edit/1",
"uiCapabilitiesPath": "visualize.show",
"uiCapabilitiesPath": "visualize_v2.show",
},
"title": "My Visualization Title 1",
},
@ -101,7 +101,7 @@ exports[`Relationships should render dashboards normally 1`] = `
"icon": "visualizeApp",
"inAppUrl": Object {
"path": "/app/visualize#/edit/2",
"uiCapabilitiesPath": "visualize.show",
"uiCapabilitiesPath": "visualize_v2.show",
},
"title": "My Visualization Title 2",
},
@ -293,7 +293,7 @@ exports[`Relationships should render index patterns normally 1`] = `
"icon": "search",
"inAppUrl": Object {
"path": "/app/discover#//1",
"uiCapabilitiesPath": "discover.show",
"uiCapabilitiesPath": "discover_v2.show",
},
"title": "My Search Title",
},
@ -306,7 +306,7 @@ exports[`Relationships should render index patterns normally 1`] = `
"icon": "visualizeApp",
"inAppUrl": Object {
"path": "/app/visualize#/edit/2",
"uiCapabilitiesPath": "visualize.show",
"uiCapabilitiesPath": "visualize_v2.show",
},
"title": "My Visualization Title",
},
@ -658,7 +658,7 @@ exports[`Relationships should render searches normally 1`] = `
"icon": "visualizeApp",
"inAppUrl": Object {
"path": "/app/visualize#/edit/2",
"uiCapabilitiesPath": "visualize.show",
"uiCapabilitiesPath": "visualize_v2.show",
},
"title": "My Visualization Title",
},
@ -811,7 +811,7 @@ exports[`Relationships should render visualizations normally 1`] = `
"icon": "dashboardApp",
"inAppUrl": Object {
"path": "/app/kibana#/dashboard/1",
"uiCapabilitiesPath": "dashboard.show",
"uiCapabilitiesPath": "dashboard_v2.show",
},
"title": "My Dashboard 1",
},
@ -824,7 +824,7 @@ exports[`Relationships should render visualizations normally 1`] = `
"icon": "dashboardApp",
"inAppUrl": Object {
"path": "/app/kibana#/dashboard/2",
"uiCapabilitiesPath": "dashboard.show",
"uiCapabilitiesPath": "dashboard_v2.show",
},
"title": "My Dashboard 2",
},

View file

@ -54,7 +54,7 @@ describe('Relationships', () => {
icon: 'search',
inAppUrl: {
path: '/app/discover#//1',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
title: 'My Search Title',
},
@ -67,7 +67,7 @@ describe('Relationships', () => {
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
title: 'My Visualization Title',
},
@ -137,7 +137,7 @@ describe('Relationships', () => {
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
title: 'My Visualization Title',
},
@ -155,7 +155,7 @@ describe('Relationships', () => {
icon: 'search',
inAppUrl: {
path: '/discover/1',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
},
},
@ -192,7 +192,7 @@ describe('Relationships', () => {
icon: 'dashboardApp',
inAppUrl: {
path: '/app/kibana#/dashboard/1',
uiCapabilitiesPath: 'dashboard.show',
uiCapabilitiesPath: 'dashboard_v2.show',
},
title: 'My Dashboard 1',
},
@ -205,7 +205,7 @@ describe('Relationships', () => {
icon: 'dashboardApp',
inAppUrl: {
path: '/app/kibana#/dashboard/2',
uiCapabilitiesPath: 'dashboard.show',
uiCapabilitiesPath: 'dashboard_v2.show',
},
title: 'My Dashboard 2',
},
@ -223,7 +223,7 @@ describe('Relationships', () => {
icon: 'visualizeApp',
inAppUrl: {
path: '/edit/1',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
},
},
@ -260,7 +260,7 @@ describe('Relationships', () => {
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/1',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
title: 'My Visualization Title 1',
},
@ -273,7 +273,7 @@ describe('Relationships', () => {
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
title: 'My Visualization Title 2',
},
@ -291,7 +291,7 @@ describe('Relationships', () => {
icon: 'dashboardApp',
inAppUrl: {
path: '/dashboard/1',
uiCapabilitiesPath: 'dashboard.show',
uiCapabilitiesPath: 'dashboard_v2.show',
},
},
},
@ -332,7 +332,7 @@ describe('Relationships', () => {
icon: 'dashboardApp',
inAppUrl: {
path: '/dashboard/1',
uiCapabilitiesPath: 'dashboard.show',
uiCapabilitiesPath: 'dashboard_v2.show',
},
},
},

View file

@ -181,7 +181,7 @@ describe('SavedObjectsTable', () => {
icon: 'search',
inAppUrl: {
path: '/discover/2',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
},
},
@ -193,7 +193,7 @@ describe('SavedObjectsTable', () => {
icon: 'dashboardApp',
inAppUrl: {
path: '/dashboard/3',
uiCapabilitiesPath: 'dashboard.show',
uiCapabilitiesPath: 'dashboard_v2.show',
},
},
},
@ -205,7 +205,7 @@ describe('SavedObjectsTable', () => {
icon: 'visualizeApp',
inAppUrl: {
path: '/edit/4',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
},
},
@ -490,7 +490,7 @@ describe('SavedObjectsTable', () => {
editUrl: '/management/kibana/objects/savedSearches/2',
inAppUrl: {
path: '/discover/2',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
},
} as SavedObjectWithMetadata);
@ -506,7 +506,7 @@ describe('SavedObjectsTable', () => {
icon: 'search',
inAppUrl: {
path: '/discover/2',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
},
});

View file

@ -41,7 +41,7 @@ export function getSavedSearchObjectType(
getInAppUrl(obj) {
return {
path: `/app/discover#/view/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
};
},
},

View file

@ -66,7 +66,7 @@ async function mountComponent({
const services = {
...unifiedHistogramServicesMock,
capabilities: {
dashboard: {
dashboard_v2: {
showWriteControls: hasDashboardPermissions ?? true,
},
} as unknown as Capabilities,

View file

@ -276,7 +276,7 @@ export function Chart({
const canEditVisualizationOnTheFly = canCustomizeVisualization && chartVisible;
const canSaveVisualization =
canEditVisualizationOnTheFly && services.capabilities.dashboard?.showWriteControls;
canEditVisualizationOnTheFly && services.capabilities.dashboard_v2?.showWriteControls;
const actions: IconButtonGroupProps['buttons'] = [];

View file

@ -77,6 +77,16 @@ describe('Querybar Menu component', () => {
},
};
const services = {
application: {
...startMock.application,
capabilities: {
...startMock.application.capabilities,
savedQueryManagement: {
showQueries: true,
saveQuery: true,
},
},
},
data: dataMock,
storage: getStorage(storageValue),
uiSettings: startMock.uiSettings,

View file

@ -217,8 +217,10 @@ export function useQueryBarMenuPanels({
setRenderedComponent,
}: QueryBarMenuPanelsProps) {
const kibana = useKibana<IUnifiedSearchPluginServices>();
const { appName, usageCollection, uiSettings, http, storage } = kibana.services;
const { appName, usageCollection, uiSettings, http, storage, application } = kibana.services;
const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName);
const showSavedQueries =
showQueryInput && showFilterBar && application.capabilities.savedQueryManagement?.showQueries;
const cancelPendingListingRequest = useRef<() => void>(() => {});
const [hasSavedQueries, setHasSavedQueries] = useState(false);
@ -246,10 +248,10 @@ export function useQueryBarMenuPanels({
setHasSavedQueries(queryCount > 0);
};
if (showQueryInput && showFilterBar) {
if (showSavedQueries) {
fetchSavedQueries();
}
}, [savedQueryService, showQueryInput, showFilterBar]);
}, [savedQueryService, showSavedQueries]);
useEffect(() => {
if (savedQuery) {
@ -430,7 +432,7 @@ export function useQueryBarMenuPanels({
}
// saved queries actions are only shown when the showQueryInput and showFilterBar is true
if (showQueryInput && showFilterBar) {
if (showSavedQueries) {
items.push(...queryAndFiltersRelatedPanels);
}

View file

@ -22,7 +22,7 @@ import { useFilterManager } from './lib/use_filter_manager';
import { useTimefilter } from './lib/use_timefilter';
import { useSavedQuery } from './lib/use_saved_query';
import { useQueryStringManager } from './lib/use_query_string_manager';
import { type SavedQueryMenuVisibility, canShowSavedQuery } from './lib/can_show_saved_query';
import { canShowSavedQuery } from './lib/can_show_saved_query';
import type { UnifiedSearchPublicPluginStart } from '../types';
export interface StatefulSearchBarDeps {
@ -41,7 +41,12 @@ export type StatefulSearchBarProps<QT extends Query | AggregateQuery = Query> =
appName: string;
useDefaultBehaviors?: boolean;
savedQueryId?: string;
saveQueryMenuVisibility?: SavedQueryMenuVisibility;
/**
* Determines if saving queries is allowed within the saved query management popover (still requires privileges).
* This does not impact if queries can be loaded, which is determined by the saved query management read privilege.
* Defaults to false.
*/
allowSavingQueries?: boolean;
onSavedQueryIdChange?: (savedQueryId?: string) => void;
onFiltersUpdated?: (filters: Filter[]) => void;
};
@ -155,7 +160,7 @@ export function createSearchBar({
// App name should come from the core application service.
// Until it's available, we'll ask the user to provide it for the pre-wired component.
return <QT extends AggregateQuery | Query = Query>(props: StatefulSearchBarProps<QT>) => {
const { useDefaultBehaviors } = props;
const { useDefaultBehaviors, allowSavingQueries } = props;
// Handle queries
const onQuerySubmitRef = useRef(props.onQuerySubmit);
@ -200,7 +205,7 @@ export function createSearchBar({
}, [query, timeRange, useDefaultBehaviors]);
const showSaveQuery = canShowSavedQuery({
saveQueryMenuVisibility: props.saveQueryMenuVisibility,
allowSavingQueries,
query,
core,
});

View file

@ -12,21 +12,21 @@ import { canShowSavedQuery } from './can_show_saved_query';
const core = coreMock.createStart();
function getCore(saveQueryGloballyAllowed: boolean): typeof core {
function getCore(saveQueryManagementAllowed: boolean): typeof core {
return {
...core,
application: {
...core.application,
capabilities: {
...core.application.capabilities,
savedQueryManagement: { saveQuery: saveQueryGloballyAllowed },
savedQueryManagement: { saveQuery: saveQueryManagementAllowed },
},
},
};
}
const coreWithoutGlobalPrivilege = getCore(false);
const coreWithGlobalPrivilege = getCore(true);
const coreWithoutSavedQueryManagement = getCore(false);
const coreWithSavedQueryManagement = getCore(true);
const kqlQuery = {
query: 'response:200',
@ -38,70 +38,41 @@ const esqlQuery = {
};
describe('canShowSaveQuery', () => {
it('should allow when allowed_by_app_privilege', async () => {
it('should return false if allowSavingQueries is not true', async () => {
expect(
canShowSavedQuery({
core: coreWithoutGlobalPrivilege,
core: coreWithSavedQueryManagement,
query: kqlQuery,
})
);
});
it('should return true with saved query management privilege', async () => {
expect(
canShowSavedQuery({
allowSavingQueries: true,
core: coreWithSavedQueryManagement,
query: kqlQuery,
saveQueryMenuVisibility: 'allowed_by_app_privilege',
})
).toBe(true);
});
it('should not allow for text-based queries when allowed_by_app_privilege', async () => {
it('should return false without saved query management privilege', async () => {
expect(
canShowSavedQuery({
core: coreWithoutGlobalPrivilege,
allowSavingQueries: true,
core: coreWithoutSavedQueryManagement,
query: kqlQuery,
})
).toBe(false);
});
it('should return false for ES|QL queries', async () => {
expect(
canShowSavedQuery({
allowSavingQueries: true,
core: coreWithSavedQueryManagement,
query: esqlQuery,
saveQueryMenuVisibility: 'allowed_by_app_privilege',
})
).toBe(false);
});
it('should not allow for text-based queries when globally_managed', async () => {
expect(
canShowSavedQuery({
core: coreWithGlobalPrivilege,
query: esqlQuery,
saveQueryMenuVisibility: 'globally_managed',
})
).toBe(false);
});
it('should allow when globally allowed', async () => {
expect(
canShowSavedQuery({
core: coreWithGlobalPrivilege,
query: kqlQuery,
saveQueryMenuVisibility: 'globally_managed',
})
).toBe(true);
});
it('should not allow when globally disallowed', async () => {
expect(
canShowSavedQuery({
core: coreWithoutGlobalPrivilege,
query: kqlQuery,
saveQueryMenuVisibility: 'globally_managed',
})
).toBe(false);
});
it('should not allow when hidden', async () => {
expect(
canShowSavedQuery({
core: coreWithGlobalPrivilege,
query: kqlQuery,
saveQueryMenuVisibility: 'hidden',
})
).toBe(false);
expect(
canShowSavedQuery({
core: coreWithGlobalPrivilege,
query: kqlQuery,
saveQueryMenuVisibility: undefined,
})
).toBe(false);
});

View file

@ -11,36 +11,28 @@ import type { AggregateQuery, Query } from '@kbn/es-query';
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import { isOfAggregateQueryType } from '@kbn/es-query';
export type SavedQueryMenuVisibility =
| 'hidden'
| 'globally_managed' // managed by "Saved Query Management" global privilege
| 'allowed_by_app_privilege'; // use only if your Kibana app grants this privilege, otherwise default to `globally_managed`
/**
* Determines if saving queries is allowed within the saved query management popover (still requires privileges).
* This does not impact if queries can be loaded, which is determined by the saved query management read privilege.
*/
export const canShowSavedQuery = ({
saveQueryMenuVisibility = 'hidden',
allowSavingQueries = false,
query,
core,
}: {
saveQueryMenuVisibility?: SavedQueryMenuVisibility;
allowSavingQueries?: boolean;
query: AggregateQuery | Query | { [key: string]: any };
core: CoreStart;
}): boolean => {
// don't show Saved Query menu by default
if (!saveQueryMenuVisibility || saveQueryMenuVisibility === 'hidden') {
// Don't allow saving queries by default
if (!allowSavingQueries) {
return false;
}
// Saved Queries are not supported for text-based languages (only Saved Searches)
// Saved Queries are not supported for ES|QL (only Saved Searches)
if (isOfAggregateQueryType(query)) {
return false;
}
const isAllowedGlobally = Boolean(core.application.capabilities.savedQueryManagement?.saveQuery);
// users can allow saving queries globally or grant permission per app
if (saveQueryMenuVisibility === 'allowed_by_app_privilege') {
return true;
}
return isAllowedGlobally;
return Boolean(core.application.capabilities.savedQueryManagement?.saveQuery);
};

View file

@ -74,6 +74,16 @@ function wrapSearchBarInContext(testProps: any) {
(dataViewEditorMock.userPermissions.editDataView as jest.Mock).mockReturnValue(true);
const services = {
application: {
...startMock.application,
capabilities: {
...startMock.application.capabilities,
savedQueryManagement: {
showQueries: true,
saveQuery: true,
},
},
},
uiSettings: startMock.uiSettings,
settings: startMock.settings,
savedObjects: startMock.savedObjects,

View file

@ -145,7 +145,7 @@ export class EditInLensAction implements Action<EmbeddableApiContext> {
return false;
const vis = embeddable.getVis();
const { visualize } = getCapabilities();
const { visualize_v2: visualize } = getCapabilities();
if (!vis || !visualize.show) {
return false;
}

View file

@ -77,9 +77,9 @@ export function initializeEditApi({
const isByValue = !savedObjectId$.getValue();
if (isByValue)
return Boolean(
capabilities.dashboard?.showWriteControls && capabilities.visualize?.show
capabilities.dashboard_v2?.showWriteControls && capabilities.visualize_v2?.show
);
else return Boolean(capabilities.visualize?.save);
else return Boolean(capabilities.visualize_v2?.save);
},
};
}

View file

@ -58,9 +58,9 @@ export const createVisEmbeddableFromObject =
}
const capabilities = {
visualizeSave: Boolean(getCapabilities().visualize.save),
dashboardSave: Boolean(getCapabilities().dashboard?.showWriteControls),
visualizeOpen: Boolean(getCapabilities().visualize?.show),
visualizeSave: Boolean(getCapabilities().visualize_v2.save),
dashboardSave: Boolean(getCapabilities().dashboard_v2?.showWriteControls),
visualizeOpen: Boolean(getCapabilities().visualize_v2?.show),
};
return createVisualizeEmbeddableAsync(

View file

@ -326,8 +326,8 @@ export class VisualizationsPlugin
navigation: pluginsStart.navigation,
share: pluginsStart.share,
toastNotifications: coreStart.notifications.toasts,
visualizeCapabilities: coreStart.application.capabilities.visualize,
dashboardCapabilities: coreStart.application.capabilities.dashboard,
visualizeCapabilities: coreStart.application.capabilities.visualize_v2,
dashboardCapabilities: coreStart.application.capabilities.dashboard_v2,
embeddable: pluginsStart.embeddable,
stateTransferService: pluginsStart.embeddable.getStateTransfer(),
setActiveUrl,

View file

@ -334,9 +334,7 @@ const TopNav = ({
]
: undefined
}
saveQueryMenuVisibility={
services.visualizeCapabilities.saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
}
allowSavingQueries
dataViewPickerComponentProps={
shouldShowDataViewPicker && vis.data.indexPattern
? {
@ -375,7 +373,6 @@ const TopNav = ({
setMenuMountPoint={setHeaderActionMenu}
indexPatterns={indexPatterns}
showSearchBar
saveQueryMenuVisibility="hidden"
showDatePicker={false}
showQueryInput={false}
/>

View file

@ -21,7 +21,7 @@ import { createEmbeddableStateTransferMock } from '@kbn/embeddable-plugin/public
import { visualizeAppStateStub } from './stubs';
describe('showPublicUrlSwitch', () => {
test('returns false if "visualize" app is not available', () => {
test('returns false if "visualize_v2" app is not available', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
@ -32,12 +32,12 @@ describe('showPublicUrlSwitch', () => {
expect(result).toBe(false);
});
test('returns false if "visualize" app is not accessible', () => {
test('returns false if "visualize_v2" app is not accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
visualize: {
visualize_v2: {
show: false,
},
};
@ -46,12 +46,12 @@ describe('showPublicUrlSwitch', () => {
expect(result).toBe(false);
});
test('returns true if "visualize" app is not available an accessible', () => {
test('returns true if "visualize_v2" app is not available an accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
visualize: {
visualize_v2: {
show: true,
},
};

View file

@ -49,7 +49,6 @@ interface VisualizeCapabilities {
createShortUrl: boolean;
delete: boolean;
save: boolean;
saveQuery: boolean;
show: boolean;
}
@ -76,9 +75,9 @@ export interface TopNavConfigParams {
const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard);
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
if (!anonymousUserCapabilities.visualize) return false;
if (!anonymousUserCapabilities.visualize_v2) return false;
const visualize = anonymousUserCapabilities.visualize as unknown as VisualizeCapabilities;
const visualize = anonymousUserCapabilities.visualize_v2 as unknown as VisualizeCapabilities;
return !!visualize.show;
};

View file

@ -8,11 +8,10 @@
*/
export const capabilitiesProvider = () => ({
visualize: {
visualize_v2: {
show: true,
createShortUrl: true,
delete: true,
save: true,
saveQuery: true,
},
});

View file

@ -22,7 +22,7 @@ test('should return visualize edit url', () => {
} as unknown as VisualizationSavedObject;
expect(getInAppUrl(obj)).toEqual({
path: '/app/visualize#/edit/1',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
});
});

View file

@ -25,6 +25,6 @@ export function getInAppUrl(obj: VisualizationSavedObject) {
? undefined
: {
path: `/app/visualize#/edit/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
};
}

View file

@ -230,7 +230,7 @@ export default function ({ getService }: FtrProviderContext) {
hiddenType: false,
inAppUrl: {
path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
namespaceType: 'multiple-isolated',
});
@ -249,7 +249,7 @@ export default function ({ getService }: FtrProviderContext) {
hiddenType: false,
inAppUrl: {
path: '/app/dashboards#/view/b70c7ae0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'dashboard.show',
uiCapabilitiesPath: 'dashboard_v2.show',
},
namespaceType: 'multiple-isolated',
});
@ -268,7 +268,7 @@ export default function ({ getService }: FtrProviderContext) {
hiddenType: false,
inAppUrl: {
path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
});
@ -278,7 +278,7 @@ export default function ({ getService }: FtrProviderContext) {
hiddenType: false,
inAppUrl: {
path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
});

View file

@ -108,7 +108,7 @@ export default function ({ getService }: FtrProviderContext) {
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -150,7 +150,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'VisualizationFromSavedSearch',
inAppUrl: {
path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -196,7 +196,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'Visualization',
inAppUrl: {
path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -211,7 +211,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'VisualizationFromSavedSearch',
inAppUrl: {
path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -235,7 +235,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'Visualization',
inAppUrl: {
path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -250,7 +250,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'VisualizationFromSavedSearch',
inAppUrl: {
path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -296,7 +296,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'OneRecord',
inAppUrl: {
path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -311,7 +311,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'Dashboard',
inAppUrl: {
path: '/app/dashboards#/view/b70c7ae0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'dashboard.show',
uiCapabilitiesPath: 'dashboard_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -337,7 +337,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'OneRecord',
inAppUrl: {
path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -383,7 +383,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'OneRecord',
inAppUrl: {
path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -398,7 +398,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'Visualization',
inAppUrl: {
path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -424,7 +424,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'OneRecord',
inAppUrl: {
path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'discover.show',
uiCapabilitiesPath: 'discover_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,
@ -476,7 +476,7 @@ export default function ({ getService }: FtrProviderContext) {
icon: 'visualizeApp',
inAppUrl: {
path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'visualize.show',
uiCapabilitiesPath: 'visualize_v2.show',
},
namespaceType: 'multiple-isolated',
hiddenType: false,

View file

@ -192,6 +192,14 @@ export class SavedQueryManagementComponentService extends FtrService {
await this.closeSavedQueryManagementComponent();
}
async savedQueryLoadButtonMissingOrFail() {
await this.retry.try(async () => {
await this.openSavedQueryManagementComponent();
await this.testSubjects.missingOrFail('saved-query-management-load-button');
});
await this.closeSavedQueryManagementComponent();
}
async openSavedQueryManagementComponent() {
const isOpenAlready = await this.testSubjects.exists('queryBarMenuPanel');
if (isOpenAlready) return;
@ -220,11 +228,16 @@ export class SavedQueryManagementComponentService extends FtrService {
});
}
async saveNewQueryMissingOrFail() {
async saveNewQueryMissingOrFail(expectedButtonState: 'disabled' | 'hidden' = 'disabled') {
await this.openSavedQueryManagementComponent();
const saveFilterSetBtn = await this.testSubjects.find('saved-query-management-save-button');
const isDisabled = await saveFilterSetBtn.getAttribute('disabled');
expect(isDisabled).to.equal('true');
if (expectedButtonState === 'disabled') {
const saveFilterSetBtn = await this.testSubjects.find('saved-query-management-save-button');
const isDisabled = await saveFilterSetBtn.getAttribute('disabled');
expect(isDisabled).to.equal('true');
} else {
await this.testSubjects.missingOrFail('saved-query-management-save-button');
}
}
async updateCurrentlyLoadedQueryMissingOrFail() {

View file

@ -40,7 +40,7 @@ export function getActions(
mlTimefilterRefresh$.next(refresh);
};
// Navigate to Lens with prefilled chart for data field
if (services.application?.capabilities?.visualize?.show === true && lensPlugin !== undefined) {
if (services.application?.capabilities?.visualize_v2?.show === true && lensPlugin !== undefined) {
const canUseLensEditor = lensPlugin?.canUseEditor();
actions.push({
name: i18n.translate('xpack.dataVisualizer.index.dataGrid.exploreInLensTitle', {
@ -69,7 +69,7 @@ export function getActions(
if (
services?.uiActions &&
mapsPlugin &&
services.application?.capabilities?.maps?.show === true
services.application?.capabilities?.maps_v2?.show === true
) {
actions.push({
name: i18n.translate('xpack.dataVisualizer.index.dataGrid.exploreInMapsTitle', {

View file

@ -98,7 +98,7 @@ export const ResultsLinks: FC<Props> = ({
let unmounted = false;
const getDiscoverUrl = async (): Promise<void> => {
const isDiscoverAvailable = capabilities.discover?.show ?? false;
const isDiscoverAvailable = capabilities.discover_v2?.show ?? false;
if (!isDiscoverAvailable) return;
const discoverLocator = url.locators.get('DISCOVER_APP_LOCATOR');

View file

@ -56,7 +56,7 @@ export const ActionsPanel: FC<Props> = ({
const dataViewId = dataView.id;
const dataViewIndexPattern = dataView.getIndexPattern();
const getDiscoverUrl = async (): Promise<void> => {
const isDiscoverAvailable = capabilities.discover?.show ?? false;
const isDiscoverAvailable = capabilities.discover_v2?.show ?? false;
if (!isDiscoverAvailable) return;
const discoverLocator = url?.locators.get('DISCOVER_APP_LOCATOR');

View file

@ -114,7 +114,7 @@ export abstract class AbstractExploreDataAction {
const { core, plugins } = this.params.start();
const { capabilities } = core.application;
if (capabilities.discover && !capabilities.discover.show) return false;
if (capabilities.discover_v2 && !capabilities.discover_v2.show) return false;
if (!plugins.discover.locator) return false;
return shared.hasExactlyOneDataView(embeddable);

View file

@ -177,7 +177,7 @@ describe('"Explore underlying data" panel action', () => {
const { action, context, core } = setup();
core.application.capabilities = { ...core.application.capabilities };
(core.application.capabilities as any).discover = {
(core.application.capabilities as any).discover_v2 = {
show: false,
};

View file

@ -165,7 +165,7 @@ describe('"Explore underlying data" panel action', () => {
const { action, context, core } = setup();
core.application.capabilities = { ...core.application.capabilities };
(core.application.capabilities as any).discover = {
(core.application.capabilities as any).discover_v2 = {
show: false,
};

View file

@ -14,7 +14,7 @@ jest.mock('../../legacy_shims', () => ({
Legacy: {
shims: {
getBasePath: () => '',
capabilities: { discover: { show: true } },
capabilities: { discover_v2: { show: true } },
},
},
}));

View file

@ -209,7 +209,7 @@ export class LogsContent extends PureComponent<LogsContentProps> {
renderCallout() {
const { capabilities: uiCapabilities, kibanaServices } = Legacy.shims;
const show = uiCapabilities.discover && uiCapabilities.discover.show;
const show = uiCapabilities.discover_v2 && uiCapabilities.discover_v2.show;
const {
logs: { enabled },

View file

@ -85,7 +85,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
const toastNotifications = useToastNotifications();
const { application, share, ...startServices } = useAppDependencies();
const isDiscoverAvailable = application.capabilities.discover?.show ?? false;
const isDiscoverAvailable = application.capabilities.discover_v2?.show ?? false;
useEffect(() => {
let unmounted = false;

View file

@ -25,7 +25,7 @@ export const useDiscoverAction = (forceDisable: boolean) => {
share,
application: { capabilities },
} = useAppDependencies();
const isDiscoverAvailable = !!capabilities.discover?.show;
const isDiscoverAvailable = !!capabilities.discover_v2?.show;
const { data: dataViewsTitleIdMap } = useGetDataViewsTitleIdMap();

View file

@ -17459,11 +17459,9 @@
"xpack.exploratoryView.ux.addDataButtonLabel": "Ajouter des données d'expérience utilisateur",
"xpack.exploratoryView.uxLabel": "Expérience utilisateur (RUM)",
"xpack.features.advancedSettingsFeatureName": "Paramètres avancés",
"xpack.features.dashboardFeatureName": "Dashboard",
"xpack.features.dataViewFeatureName": "Gestion de la vue de données",
"xpack.features.devToolsFeatureName": "Outils de développement",
"xpack.features.devToolsPrivilegesTooltip": "L'utilisateur doit également recevoir les privilèges de cluster et d'index Elasticsearch appropriés.",
"xpack.features.discoverFeatureName": "Discover",
"xpack.features.filesManagementFeatureName": "Gestion des fichiers",
"xpack.features.filesSharedImagesFeatureName": "Images partagées",
"xpack.features.filesSharedImagesPrivilegesTooltip": "Requis pour accéder aux images stockées dans Kibana.",
@ -17484,7 +17482,6 @@
"xpack.features.savedObjectsManagementFeatureName": "Gestion des objets enregistrés",
"xpack.features.savedQueryManagementFeatureName": "Gestion des requêtes enregistrées",
"xpack.features.savedQueryManagementTooltip": "Si \"All\" (Tout) est défini, les requêtes enregistrées peuvent être gérées grâce à Kibana dans toutes les applications compatibles. Si \"None\" est défini, les privilèges relatifs aux requêtes enregistrées sont fixés indépendamment pour chaque application.",
"xpack.features.visualizeFeatureName": "Bibliothèque Visualize",
"xpack.fileUpload.dataViewAlreadyExistsErrorMessage": "La vue de données existe déjà.",
"xpack.fileUpload.fileSizeError": "La taille du fichier {fileSize} dépasse la taille maximale de fichier de {maxFileSize}",
"xpack.fileUpload.fileTypeError": "Le fichier ne fait pas partie des types acceptables :{types}",
@ -24966,7 +24963,6 @@
"xpack.maps.esSearch.topHitsSizeMsg": "Affichage des {topHitsSize} premiers documents par entité.",
"xpack.maps.feature.appDescription": "Explorez les données géospatiales d'Elasticsearch et de l'Elastic Maps Service.",
"xpack.maps.featureCatalogue.mapsSubtitle": "Tracez les données géographiques.",
"xpack.maps.featureRegistry.mapsFeatureName": "Maps",
"xpack.maps.fields.percentileMedianLabek": "médiane",
"xpack.maps.fileUpload.trimmedResultsMsg": "Résultats limités à {numFeatures} fonctionnalités, {previewCoverage} % du fichier.",
"xpack.maps.fileUploadWizard.configureDocumentLayerLabel": "Ajouter comme calque du document",

View file

@ -17318,11 +17318,9 @@
"xpack.exploratoryView.ux.addDataButtonLabel": "UXデータを追加",
"xpack.exploratoryView.uxLabel": "ユーザーエクスペリエンスRUM",
"xpack.features.advancedSettingsFeatureName": "高度な設定",
"xpack.features.dashboardFeatureName": "ダッシュボード",
"xpack.features.dataViewFeatureName": "データビュー管理",
"xpack.features.devToolsFeatureName": "開発ツール",
"xpack.features.devToolsPrivilegesTooltip": "また、ユーザーに適切な Elasticsearch クラスターとインデックスの権限が与えられている必要があります。",
"xpack.features.discoverFeatureName": "Discover",
"xpack.features.filesManagementFeatureName": "ファイル管理",
"xpack.features.filesSharedImagesFeatureName": "共有画像",
"xpack.features.filesSharedImagesPrivilegesTooltip": "Kibanaで保存された画像にアクセスする必要があります。",
@ -17343,7 +17341,6 @@
"xpack.features.savedObjectsManagementFeatureName": "保存されたオブジェクトの管理",
"xpack.features.savedQueryManagementFeatureName": "保存されたクエリー管理",
"xpack.features.savedQueryManagementTooltip": "すべてに設定すると、保存されたクエリーは、クエリーをサポートするすべてのアプリケーションのKibana全体で管理できます。なしに設定すると、保存されたクエリー権限は各アプリケーションで独自に決定されます。",
"xpack.features.visualizeFeatureName": "Visualizeライブラリ",
"xpack.fileUpload.dataViewAlreadyExistsErrorMessage": "データビューはすでに存在します。",
"xpack.fileUpload.fileSizeError": "ファイルサイズ{fileSize}は最大ファイルサイズの{maxFileSize}を超えています",
"xpack.fileUpload.fileTypeError": "ファイルは使用可能なタイプのいずれかではありません。{types}",
@ -24832,7 +24829,6 @@
"xpack.maps.esSearch.topHitsSizeMsg": "エンティティごとに上位の{topHitsSize}ドキュメントを表示しています。",
"xpack.maps.feature.appDescription": "ElasticsearchとElastic Maps Serviceの地理空間データを閲覧します。",
"xpack.maps.featureCatalogue.mapsSubtitle": "地理的なデータをプロットします。",
"xpack.maps.featureRegistry.mapsFeatureName": "マップ",
"xpack.maps.fields.percentileMedianLabek": "中間",
"xpack.maps.fileUpload.trimmedResultsMsg": "結果は{numFeatures}個の特徴量に制限されます。これはファイルの{previewCoverage}%です。",
"xpack.maps.fileUploadWizard.configureDocumentLayerLabel": "ドキュメントレイヤーとして追加",

View file

@ -17040,11 +17040,9 @@
"xpack.exploratoryView.ux.addDataButtonLabel": "添加 UX 数据",
"xpack.exploratoryView.uxLabel": "用户体验 (RUM)",
"xpack.features.advancedSettingsFeatureName": "高级设置",
"xpack.features.dashboardFeatureName": "仪表板",
"xpack.features.dataViewFeatureName": "数据视图管理",
"xpack.features.devToolsFeatureName": "开发工具",
"xpack.features.devToolsPrivilegesTooltip": "还应向用户授予适当的 Elasticsearch 集群和索引权限",
"xpack.features.discoverFeatureName": "Discover",
"xpack.features.filesManagementFeatureName": "文件管理",
"xpack.features.filesSharedImagesFeatureName": "共享图像",
"xpack.features.filesSharedImagesPrivilegesTooltip": "访问存储在 Kibana 中的图像时需要此项。",
@ -17065,7 +17063,6 @@
"xpack.features.savedObjectsManagementFeatureName": "已保存对象管理",
"xpack.features.savedQueryManagementFeatureName": "已保存查询管理",
"xpack.features.savedQueryManagementTooltip": "如果设置为'全部',可以在支持已保存查询的所有应用程序中管理整个 Kibana 中的已保存查询。如果设置为'无',将由每个应用程序单独确定已保存查询权限。",
"xpack.features.visualizeFeatureName": "Visualize 库",
"xpack.fileUpload.dataViewAlreadyExistsErrorMessage": "数据视图已存在。",
"xpack.fileUpload.fileSizeError": "文件大小 {fileSize} 超过最大文件大小 {maxFileSize}",
"xpack.fileUpload.fileTypeError": "文件不是可接受类型之一:{types}",
@ -24409,7 +24406,6 @@
"xpack.maps.esSearch.topHitsSizeMsg": "显示每个实体排名前 {topHitsSize} 的文档。",
"xpack.maps.feature.appDescription": "从 Elasticsearch 和 Elastic Maps Service 浏览地理空间数据。",
"xpack.maps.featureCatalogue.mapsSubtitle": "绘制地理数据。",
"xpack.maps.featureRegistry.mapsFeatureName": "Maps",
"xpack.maps.fields.percentileMedianLabek": "中值",
"xpack.maps.fileUpload.trimmedResultsMsg": "结果仅限于 {numFeatures} 个特征、{previewCoverage}% 的文件。",
"xpack.maps.fileUploadWizard.configureDocumentLayerLabel": "添加为文档层",

View file

@ -188,7 +188,7 @@ const FieldPanel: FC<FieldPanelProps> = ({
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false);
const [isDashboardFormValid, setIsDashboardFormValid] = useState(true);
const canEditDashboards = capabilities.dashboard?.createNew ?? false;
const canEditDashboards = capabilities.dashboard_v2?.createNew ?? false;
const { create: canCreateCase, update: canUpdateCase } = cases?.helpers?.canUseCases() ?? {
create: false,
update: false,

View file

@ -74,7 +74,7 @@ export const AttachmentsMenu = ({
const timeRange = useTimeRangeUpdates();
const canEditDashboards = capabilities.dashboard.createNew;
const canEditDashboards = capabilities.dashboard_v2.createNew;
const onSave: SaveModalDashboardProps['onSave'] = useCallback(
({ dashboardId, newTitle, newDescription }) => {

View file

@ -66,7 +66,7 @@ export const LogRateAnalysisAttachmentsMenu = ({
CASES_TOAST_MESSAGES_TITLES.LOG_RATE_ANALYSIS
);
const canEditDashboards = capabilities.dashboard.createNew;
const canEditDashboards = capabilities.dashboard_v2.createNew;
const { create: canCreateCase, update: canUpdateCase } = cases?.helpers?.canUseCases() ?? {
create: false,

View file

@ -38,7 +38,7 @@ export const useViewInDiscoverAction = (dataViewId?: string): TableItemAction =>
const { timeRange } = useFilterQueryUpdates();
const discoverUrlError = useMemo(() => {
if (!application.capabilities.discover?.show) {
if (!application.capabilities.discover_v2?.show) {
const discoverNotEnabled = i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTable.discoverNotEnabledErrorMessage',
{
@ -68,7 +68,7 @@ export const useViewInDiscoverAction = (dataViewId?: string): TableItemAction =>
return autoGeneratedDiscoverLinkError;
}
}, [application.capabilities.discover?.show, dataViewId, discoverLocator]);
}, [application.capabilities.discover_v2?.show, dataViewId, discoverLocator]);
const generateDiscoverUrl = async (groupTableItem: GroupTableItem | SignificantItem) => {
if (discoverLocator !== undefined) {

View file

@ -196,19 +196,22 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
reopenCase: permissions.reopenCase,
createComment: permissions.createComment,
},
visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show },
visualize: {
crud: !!capabilities.visualize_v2?.save,
read: !!capabilities.visualize_v2?.show,
},
dashboard: {
crud: !!capabilities.dashboard?.createNew,
read: !!capabilities.dashboard?.show,
crud: !!capabilities.dashboard_v2?.createNew,
read: !!capabilities.dashboard_v2?.show,
},
}),
[
capabilities.actions?.save,
capabilities.actions?.show,
capabilities.dashboard?.createNew,
capabilities.dashboard?.show,
capabilities.visualize?.save,
capabilities.visualize?.show,
capabilities.dashboard_v2?.createNew,
capabilities.dashboard_v2?.show,
capabilities.visualize_v2?.save,
capabilities.visualize_v2?.show,
permissions.all,
permissions.create,
permissions.read,

View file

@ -94,8 +94,8 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta
case_reopen: true,
create_comment: true,
},
visualize: { save: true, show: true },
dashboard: { show: true, createNew: true },
visualize_v2: { save: true, show: true },
dashboard_v2: { show: true, createNew: true },
};
return services;

Some files were not shown because too many files have changed in this diff Show more