Feature Controls - Read only badges (#35252)

* Adding readonly badge to the discover application

* Dashboard get a badge

* Visualize gets a badge

* Timelion gets a badge

* Canvas gets a badge

* Maps gets a badge

* Infra gets a badge

* Graph gets a badge

* Dev Tools gets a badge

* Index Patterns get badges

* Advanced Settings get badges

* Infra and i18n are super chill friends

* Using proper i18n prefix for xpack

* Adding badges to the uptime application

* APM gets a badge!

* Adding functional tests for the discover read-only badge

* Functional tests for everyone!

* Removing unused import

* Fixing chrome service mock

* Switching from ChromeBadge | null to ChromeBadge | undefined

* Fixing canvas badge assertst

* Fixing Logs ui capabilities

* More ChromeBrand | null to ChromeBrand | undefined related changes

* Using named badges

* Revert "Using named badges"

This reverts commit c0e341bee1.

* i18n'ing the uptime read-only badges

* Adding ChromeService tests for badges

* Starting to add tests for the legacy badge API

* Changing capitalization of "Read Only" to "Read only"

* Adjusting styles

* Adding $setupBadgeAutoClear tests

* Changing the badge tooltip

* Fixing timelion i18n prefix

* Changing where Canvas sets the breadcrumbs

* Using a read-only badge with an icon

* Update x-pack/plugins/canvas/public/angular/controllers/canvas.js

Co-Authored-By: kobelb <brandon.kobel@gmail.com>

* Update src/legacy/core_plugins/timelion/public/app.js

Co-Authored-By: kobelb <brandon.kobel@gmail.com>

* Changing discover's read-only verbiage

* Removing tests for code that moved to an untested part of Kibana

* Fixing issues introduced with the rebase

* Fixing priv ileges snapshot

* Adding back dropped docs

* Fixing plugin plugin doc

* Ensuring iconType is set as well

* Updating badge api, angular components moved

* graph to Graph

* Fixing linter

* Switching from aria-label to data-test-badge-label for testing

The tabIndex allows screenreaders to work properly

* Fixing eslint error

* Fixing more issues introduced by the merge from master

* APM updates badge in React hook

* Applying changes suggested by Aleh
This commit is contained in:
Brandon Kobel 2019-04-29 12:48:43 -07:00 committed by GitHub
parent 7aa698f522
commit 5ae5e3d8c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 965 additions and 89 deletions

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ChromeBadge](./kibana-plugin-public.chromebadge.md) &gt; [iconType](./kibana-plugin-public.chromebadge.icontype.md)
## ChromeBadge.iconType property
<b>Signature:</b>
```typescript
iconType?: IconType;
```

View file

@ -0,0 +1,19 @@
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ChromeBadge](./kibana-plugin-public.chromebadge.md)
## ChromeBadge interface
<b>Signature:</b>
```typescript
export interface ChromeBadge
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [iconType](./kibana-plugin-public.chromebadge.icontype.md) | <code>IconType</code> | |
| [text](./kibana-plugin-public.chromebadge.text.md) | <code>string</code> | |
| [tooltip](./kibana-plugin-public.chromebadge.tooltip.md) | <code>string</code> | |

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ChromeBadge](./kibana-plugin-public.chromebadge.md) &gt; [text](./kibana-plugin-public.chromebadge.text.md)
## ChromeBadge.text property
<b>Signature:</b>
```typescript
text: string;
```

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ChromeBadge](./kibana-plugin-public.chromebadge.md) &gt; [tooltip](./kibana-plugin-public.chromebadge.tooltip.md)
## ChromeBadge.tooltip property
<b>Signature:</b>
```typescript
tooltip: string;
```

View file

@ -1,45 +1,46 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md)
## kibana-plugin-public package
## Classes
| Class | Description |
| --- | --- |
| [FlyoutRef](./kibana-plugin-public.flyoutref.md) | A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call <code>close()</code> when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the <code>onClose</code> Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef. |
| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | |
| [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | |
## Interfaces
| Interface | Description |
| --- | --- |
| [BasePathSetup](./kibana-plugin-public.basepathsetup.md) | Provides access to the 'server.basePath' configuration option in kibana.yml |
| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
| [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) | Capabilities Setup. |
| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the start lifecycle |
| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
| [I18nSetup](./kibana-plugin-public.i18nsetup.md) | I18nSetup.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
| [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) | Provides access to the metadata injected by the server into the page |
| [OverlaySetup](./kibana-plugin-public.overlaysetup.md) | |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a <code>PluginInitializer</code> |
| [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) | The available core services passed to a plugin's <code>Plugin#setup</code> method. |
| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | |
## Type Aliases
| Type Alias | Description |
| --- | --- |
| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
| [ChromeSetup](./kibana-plugin-public.chromesetup.md) | |
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
| [ToastInput](./kibana-plugin-public.toastinput.md) | |
| [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) | |
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md)
## kibana-plugin-public package
## Classes
| Class | Description |
| --- | --- |
| [FlyoutRef](./kibana-plugin-public.flyoutref.md) | A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call <code>close()</code> when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the <code>onClose</code> Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef. |
| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | |
| [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | |
## Interfaces
| Interface | Description |
| --- | --- |
| [BasePathSetup](./kibana-plugin-public.basepathsetup.md) | Provides access to the 'server.basePath' configuration option in kibana.yml |
| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
| [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) | Capabilities Setup. |
| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | |
| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the start lifecycle |
| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
| [I18nSetup](./kibana-plugin-public.i18nsetup.md) | I18nSetup.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
| [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) | Provides access to the metadata injected by the server into the page |
| [OverlaySetup](./kibana-plugin-public.overlaysetup.md) | |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a <code>PluginInitializer</code> |
| [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) | The available core services passed to a plugin's <code>Plugin#setup</code> method. |
| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | |
## Type Aliases
| Type Alias | Description |
| --- | --- |
| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
| [ChromeSetup](./kibana-plugin-public.chromesetup.md) | |
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
| [ToastInput](./kibana-plugin-public.toastinput.md) | |
| [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) | |

View file

@ -17,7 +17,13 @@
* under the License.
*/
import { BehaviorSubject } from 'rxjs';
import { ChromeBrand, ChromeBreadcrumb, ChromeService, ChromeSetup } from './chrome_service';
import {
ChromeBadge,
ChromeBrand,
ChromeBreadcrumb,
ChromeService,
ChromeSetup,
} from './chrome_service';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<ChromeSetup> = {
@ -30,6 +36,8 @@ const createSetupContractMock = () => {
addApplicationClass: jest.fn(),
removeApplicationClass: jest.fn(),
getApplicationClasses$: jest.fn(),
getBadge$: jest.fn(),
setBadge: jest.fn(),
getBreadcrumbs$: jest.fn(),
setBreadcrumbs: jest.fn(),
getHelpExtension$: jest.fn(),
@ -39,6 +47,7 @@ const createSetupContractMock = () => {
setupContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false));
setupContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false));
setupContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name']));
setupContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge));
setupContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb]));
setupContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined));
return setupContract;

View file

@ -246,6 +246,37 @@ Array [
});
});
describe('badge', () => {
it('updates/emits the current badge', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const setup = service.setup(defaultSetupDeps());
const promise = setup
.getBadge$()
.pipe(toArray())
.toPromise();
setup.setBadge({ text: 'foo', tooltip: `foo's tooltip` });
setup.setBadge({ text: 'bar', tooltip: `bar's tooltip` });
setup.setBadge(undefined);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
undefined,
Object {
"text": "foo",
"tooltip": "foo's tooltip",
},
Object {
"text": "bar",
"tooltip": "bar's tooltip",
},
undefined,
]
`);
});
});
describe('breadcrumbs', () => {
it('updates/emits the current set of breadcrumbs', async () => {
const service = new ChromeService({ browserSupportsCsp: true });

View file

@ -22,6 +22,7 @@ import * as Url from 'url';
import { i18n } from '@kbn/i18n';
import * as Rx from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import { IconType } from '@elastic/eui';
import { InjectedMetadataSetup } from '../injected_metadata';
import { NotificationsSetup } from '../notifications';
@ -32,6 +33,13 @@ function isEmbedParamInHash() {
return Boolean(query.embed);
}
/** @public */
export interface ChromeBadge {
text: string;
tooltip: string;
iconType?: IconType;
}
/** @public */
export interface ChromeBrand {
logo?: string;
@ -75,6 +83,7 @@ export class ChromeService {
const applicationClasses$ = new Rx.BehaviorSubject<Set<string>>(new Set());
const helpExtension$ = new Rx.BehaviorSubject<ChromeHelpExtension | undefined>(undefined);
const breadcrumbs$ = new Rx.BehaviorSubject<ChromeBreadcrumb[]>([]);
const badge$ = new Rx.BehaviorSubject<ChromeBadge | undefined>(undefined);
if (!this.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) {
notifications.toasts.addWarning(
@ -175,6 +184,18 @@ export class ChromeService {
takeUntil(this.stop$)
),
/**
* Get an observable of the current badge
*/
getBadge$: () => badge$.pipe(takeUntil(this.stop$)),
/**
* Override the current badge
*/
setBadge: (badge: ChromeBadge | undefined) => {
badge$.next(badge);
},
/**
* Get an observable of the current list of breadcrumbs
*/

View file

@ -18,6 +18,7 @@
*/
export {
ChromeBadge,
ChromeBreadcrumb,
ChromeService,
ChromeSetup,

View file

@ -19,7 +19,13 @@
import { BasePathSetup } from './base_path';
import { Capabilities, CapabilitiesStart } from './capabilities';
import { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, ChromeSetup } from './chrome';
import {
ChromeBadge,
ChromeBrand,
ChromeBreadcrumb,
ChromeHelpExtension,
ChromeSetup,
} from './chrome';
import { FatalErrorsSetup } from './fatal_errors';
import { HttpSetup } from './http';
import { I18nSetup, I18nStart } from './i18n';
@ -89,6 +95,7 @@ export {
Capabilities,
CapabilitiesStart,
ChromeSetup,
ChromeBadge,
ChromeBreadcrumb,
ChromeBrand,
ChromeHelpExtension,

View file

@ -77,6 +77,7 @@ export class LegacyPlatformService {
require('ui/chrome/api/controls').__newPlatformSetup__(chrome);
require('ui/chrome/api/help_extension').__newPlatformSetup__(chrome);
require('ui/chrome/api/theme').__newPlatformSetup__(chrome);
require('ui/chrome/api/badge').__newPlatformSetup__(chrome);
require('ui/chrome/api/breadcrumbs').__newPlatformSetup__(chrome);
require('ui/chrome/services/global_nav_state').__newPlatformSetup__(chrome);

View file

@ -6,6 +6,7 @@
import * as CSS from 'csstype';
import { default } from 'react';
import { IconType } from '@elastic/eui';
import * as PropTypes from 'prop-types';
import * as Rx from 'rxjs';
import { Toast } from '@elastic/eui';
@ -32,6 +33,16 @@ export interface CapabilitiesStart {
getCapabilities: () => Capabilities;
}
// @public (undocumented)
export interface ChromeBadge {
// (undocumented)
iconType?: IconType;
// (undocumented)
text: string;
// (undocumented)
tooltip: string;
}
// @public (undocumented)
export interface ChromeBrand {
// (undocumented)

View file

@ -113,7 +113,8 @@ export default function (kibana) {
),
uiCapabilities: {
dev_tools: {
show: true
show: true,
save: true,
},
}
};

View file

@ -172,7 +172,7 @@ export default function (kibana) {
save: true
},
indexPatterns: {
createNew: true,
save: true,
},
savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({
...acc,

View file

@ -57,7 +57,22 @@ function createNewDashboardCtrl($scope, i18n) {
uiRoutes
.defaults(/dashboard/, {
requireDefaultIndex: true,
requireUICapability: 'dashboard.show'
requireUICapability: 'dashboard.show',
badge: (i18n, uiCapabilities) => {
if (uiCapabilities.dashboard.showWriteControls) {
return undefined;
}
return {
text: i18n('kbn.dashboard.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n('kbn.dashboard.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save dashboards',
}),
iconType: 'glasses'
};
}
})
.when(DashboardConstants.LANDING_PAGE_PATH, {
template: dashboardListingTemplate,

View file

@ -34,6 +34,21 @@ uiRoutes
});
uiRoutes.defaults(/^\/dev_tools(\/|$)/, {
badge: (i18n, uiCapabilities) => {
if (uiCapabilities.dev_tools.save) {
return undefined;
}
return {
text: i18n('kbn.devTools.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n('kbn.devTools.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save',
}),
iconType: 'glasses'
};
},
k7Breadcrumbs: (i18n) => [
{
text: i18n('kbn.devTools.k7BreadcrumbsDevToolsLabel', {

View file

@ -93,6 +93,21 @@ uiRoutes
? getSavedSearchBreadcrumbs
: getRootBreadcrumbs
),
badge: (i18n, uiCapabilities) => {
if (uiCapabilities.discover.save) {
return undefined;
}
return {
text: i18n('kbn.discover.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n('kbn.discover.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save searches',
}),
iconType: 'glasses'
};
}
})
.when('/discover/:id?', {
template: indexTemplate,

View file

@ -64,7 +64,7 @@ class CreateButtonComponent extends Component<Props, State> {
return null;
}
if (!uiCapabilities.indexPatterns.createNew) {
if (!uiCapabilities.indexPatterns.save) {
return null;
}

View file

@ -86,6 +86,21 @@ uiRoutes
.defaults(/management\/kibana\/(index_patterns|index_pattern)/, {
resolve: indexPatternsResolutions,
requireUICapability: 'management.kibana.index_patterns',
badge: (i18n, uiCapabilities) => {
if (uiCapabilities.indexPatterns.save) {
return undefined;
}
return {
text: i18n('kbn.management.indexPatterns.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n('kbn.management.indexPatterns.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save index patterns',
}),
iconType: 'glasses'
};
}
});
uiRoutes

View file

@ -63,6 +63,21 @@ uiRoutes
template: indexTemplate,
k7Breadcrumbs: getBreadcrumbs,
requireUICapability: 'management.kibana.settings',
badge: (i18n, uiCapabilities) => {
if (uiCapabilities.advancedSettings.save) {
return undefined;
}
return {
text: i18n('kbn.management.advancedSettings.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n('kbn.management.advancedSettings.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save advanced settings',
}),
iconType: 'glasses'
};
}
});
uiModules.get('apps/management')

View file

@ -33,6 +33,21 @@ uiRoutes
.defaults(/visualize/, {
requireDefaultIndex: true,
requireUICapability: 'visualize.show',
badge: (i18n, uiCapabilities) => {
if (uiCapabilities.visualize.save) {
return undefined;
}
return {
text: i18n('kbn.visualize.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n('kbn.visualize.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save visualizations',
}),
iconType: 'glasses'
};
}
})
.when(VisualizeConstants.LANDING_PAGE_PATH, {
template: visualizeListingTemplate,

View file

@ -72,6 +72,21 @@ require('ui/routes')
? getSavedSheetBreadcrumbs
: getCreateBreadcrumbs
),
badge: (i18n, uiCapabilities) => {
if (uiCapabilities.timelion.save) {
return undefined;
}
return {
text: i18n('timelion.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n('timelion.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save Timelion sheets',
}),
iconType: 'glasses'
};
},
resolve: {
savedSheet: function (redirectWhenMissing, savedSheets, $route) {
return savedSheets.get($route.current.params.id)

View file

@ -0,0 +1,64 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as Rx from 'rxjs';
import { ChromeBadge } from 'src/core/public/chrome';
import { chromeServiceMock } from '../../../../../core/public/mocks';
import { __newPlatformSetup__, initChromeBadgeApi } from './badge';
const newPlatformChrome = chromeServiceMock.createSetupContract();
__newPlatformSetup__(newPlatformChrome);
function setup() {
const getBadge$ = new Rx.BehaviorSubject<ChromeBadge | undefined>(undefined);
newPlatformChrome.getBadge$.mockReturnValue(getBadge$);
const chrome: any = {};
initChromeBadgeApi(chrome);
return { chrome, getBadge$ };
}
afterEach(() => {
jest.resetAllMocks();
});
describe('badge', () => {
describe('#get$()', () => {
it('returns newPlatformChrome.getBadge$()', () => {
const { chrome } = setup();
expect(chrome.badge.get$()).toBe(newPlatformChrome.getBadge$());
});
});
describe('#set()', () => {
it('calls newPlatformChrome.setBadge', () => {
const { chrome } = setup();
const badge = {
text: 'foo',
tooltip: `foo's tooltip`,
iconType: 'alert',
};
chrome.badge.set(badge);
expect(newPlatformChrome.setBadge).toHaveBeenCalledTimes(1);
expect(newPlatformChrome.setBadge).toHaveBeenCalledWith(badge);
});
});
});

View file

@ -0,0 +1,59 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Chrome } from 'ui/chrome';
import { ChromeBadge, ChromeSetup } from '../../../../../core/public';
export type Badge = ChromeBadge;
export type BadgeApi = ReturnType<typeof createBadgeApi>['badge'];
let newPlatformChrome: ChromeSetup;
export function __newPlatformSetup__(instance: ChromeSetup) {
if (newPlatformChrome) {
throw new Error('ui/chrome/api/badge is already initialized');
}
newPlatformChrome = instance;
}
function createBadgeApi() {
return {
badge: {
/**
* Get an observable that emits the current badge
* and emits each update to the badge
*/
get$() {
return newPlatformChrome.getBadge$();
},
/**
* Replace the badge with a new one
*/
set(newBadge?: Badge) {
newPlatformChrome.setBadge(newBadge);
},
},
};
}
export function initChromeBadgeApi(chrome: Chrome) {
const { badge } = createBadgeApi();
chrome.badge = badge;
}

View file

@ -36,6 +36,7 @@ import { initAngularApi } from './api/angular';
import appsApi from './api/apps';
import { initChromeControlsApi } from './api/controls';
import { initChromeNavApi } from './api/nav';
import { initChromeBadgeApi } from './api/badge';
import { initBreadcrumbsApi } from './api/breadcrumbs';
import templateApi from './api/template';
import { initChromeThemeApi } from './api/theme';
@ -70,6 +71,7 @@ initChromeXsrfApi(chrome, internals);
initChromeBasePathApi(chrome);
initChromeInjectedVarsApi(chrome);
initChromeNavApi(chrome, internals);
initChromeBadgeApi(chrome);
initBreadcrumbsApi(chrome, internals);
initLoadingCountApi(chrome, internals);
initHelpExtensionApi(chrome, internals);

View file

@ -28,3 +28,8 @@
.chrHeaderHelpMenu__version {
text-transform: none;
}
.chrHeaderBadge__wrapper {
align-self: center;
margin-right: $euiSize;
}

View file

@ -59,15 +59,17 @@ import { RecentlyAccessedHistoryItem } from 'ui/persisted_log';
import { ChromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
import { relativeToAbsolute } from 'ui/url/relative_to_absolute';
import { HeaderBadge } from './header_badge';
import { HeaderBreadcrumbs } from './header_breadcrumbs';
import { HeaderHelpMenu } from './header_help_menu';
import { HeaderNavControls } from './header_nav_controls';
import { NavControlSide } from '../';
import { ChromeBreadcrumb } from '../../../../../../../core/public';
import { ChromeBadge, ChromeBreadcrumb } from '../../../../../../../core/public';
interface Props {
appTitle?: string;
badge$: Rx.Observable<ChromeBadge | undefined>;
breadcrumbs$: Rx.Observable<ChromeBreadcrumb[]>;
homeHref: string;
isVisible: boolean;
@ -216,6 +218,7 @@ class HeaderUI extends Component<Props, State> {
public render() {
const {
appTitle,
badge$,
breadcrumbs$,
isVisible,
navControls,
@ -297,6 +300,8 @@ class HeaderUI extends Component<Props, State> {
<HeaderBreadcrumbs appTitle={appTitle} breadcrumbs$={breadcrumbs$} />
<HeaderBadge badge$={badge$} />
<EuiHeaderSection side="right">
<EuiHeaderSectionItem>
<HeaderHelpMenu helpExtension$={helpExtension$} />

View file

@ -0,0 +1,93 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { EuiBetaBadge } from '@elastic/eui';
import React, { Component } from 'react';
import * as Rx from 'rxjs';
interface Props {
badge$: Rx.Observable<ChromeBadge | undefined>;
}
interface State {
badge: ChromeBadge | undefined;
}
import { ChromeBadge } from '../../../../../../../core/public';
export class HeaderBadge extends Component<Props, State> {
private subscription?: Rx.Subscription;
constructor(props: Props) {
super(props);
this.state = { badge: undefined };
}
public componentDidMount() {
this.subscribe();
}
public componentDidUpdate(prevProps: Props) {
if (prevProps.badge$ === this.props.badge$) {
return;
}
this.unsubscribe();
this.subscribe();
}
public componentWillUnmount() {
this.unsubscribe();
}
public render() {
if (this.state.badge == null) {
return null;
}
return (
<div className="chrHeaderBadge__wrapper">
<EuiBetaBadge
data-test-subj="headerBadge"
data-test-badge-label={this.state.badge.text}
tabIndex={0}
label={this.state.badge.text}
tooltipContent={this.state.badge.tooltip}
iconType={this.state.badge.iconType}
/>
</div>
);
}
private subscribe() {
this.subscription = this.props.badge$.subscribe(badge => {
this.setState({
badge,
});
});
}
private unsubscribe() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = undefined;
}
}
}

View file

@ -38,6 +38,7 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabili
{},
// angular injected React props
{
badge$: chrome.badge.get$(),
breadcrumbs$: chrome.breadcrumbs.get$(),
helpExtension$: chrome.helpExtension.get$(),
navLinks$: chrome.getNavLinks$(),

View file

@ -19,6 +19,7 @@
import { ChromeBrand } from '../../../../core/public';
import { SavedObjectsClient } from '../saved_objects';
import { BadgeApi } from './api/badge';
import { BreadcrumbsApi } from './api/breadcrumbs';
import { HelpExtensionApi } from './api/help_extension';
import { ChromeNavLinks } from './api/nav';
@ -28,6 +29,7 @@ interface IInjector {
}
declare interface Chrome extends ChromeNavLinks {
badge: BadgeApi;
breadcrumbs: BreadcrumbsApi;
helpExtension: HelpExtensionApi;
addBasePath<T = string>(path: T): T;
@ -51,6 +53,7 @@ declare const chrome: Chrome;
// eslint-disable-next-line import/no-default-export
export default chrome;
export { Chrome };
export { Breadcrumb } from './api/breadcrumbs';
export { NavLink } from './api/nav';
export { HelpExtension } from './api/help_extension';

View file

@ -70,6 +70,7 @@ export const configureAppAngularModule = (angularModule: IModule) => {
.config($setupXsrfRequestInterceptor(newPlatform))
.run(capture$httpLoadingCount(newPlatform))
.run($setupBreadcrumbsAutoClear(newPlatform))
.run($setupBadgeAutoClear(newPlatform))
.run($setupHelpExtensionAutoClear(newPlatform))
.run($setupUrlOverflowHandling(newPlatform));
};
@ -203,6 +204,45 @@ const $setupBreadcrumbsAutoClear = (newPlatform: CoreSetup) => (
});
};
/**
* internal angular run function that will be called when angular bootstraps and
* lets us integrate with the angular router so that we can automatically clear
* the badge if we switch to a Kibana app that does not use the badge correctly
*/
const $setupBadgeAutoClear = (newPlatform: CoreSetup) => (
$rootScope: IRootScopeService,
$injector: any
) => {
// A flag used to determine if we should automatically
// clear the badge between angular route changes.
let badgeSetSinceRouteChange = false;
const $route = $injector.has('$route') ? $injector.get('$route') : {};
$rootScope.$on('$routeChangeStart', () => {
badgeSetSinceRouteChange = false;
});
$rootScope.$on('$routeChangeSuccess', () => {
const current = $route.current || {};
if (badgeSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) {
return;
}
const badgeProvider = current.badge;
if (!badgeProvider) {
newPlatform.chrome.setBadge(undefined);
return;
}
try {
newPlatform.chrome.setBadge($injector.invoke(badgeProvider));
} catch (error) {
fatalError(error);
}
});
};
/**
* internal angular run function that will be called when angular bootstraps and
* lets us integrate with the angular router so that we can automatically clear

View file

@ -17,6 +17,8 @@
* under the License.
*/
import expect from '@kbn/expect';
export function GlobalNavProvider({ getService }) {
const testSubjects = getService('testSubjects');
@ -40,5 +42,16 @@ export function GlobalNavProvider({ getService }) {
async getLastBreadcrumb() {
return await testSubjects.getVisibleText('headerGlobalNav breadcrumbs last&breadcrumb');
}
async badgeExistsOrFail(expectedLabel) {
await testSubjects.existOrFail('headerBadge');
const element = await testSubjects.find('headerBadge');
const actualLabel = await element.getAttribute('data-test-badge-label');
expect(actualLabel.toUpperCase()).to.equal(expectedLabel.toUpperCase());
}
async badgeMissingOrFail() {
await testSubjects.missingOrFail('headerBadge');
}
};
}

View file

@ -87,7 +87,7 @@ export function apm(kibana: any) {
all: [],
read: []
},
ui: ['show']
ui: ['show', 'save']
},
read: {
api: ['apm'],

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { useEffect } from 'react';
import { capabilities } from 'ui/capabilities';
import chrome from 'ui/chrome';
export const useUpdateBadgeEffect = () => {
useEffect(() => {
const uiCapabilities = capabilities.get();
chrome.badge.set(
!uiCapabilities.apm.save
? {
text: i18n.translate('xpack.apm.header.badge.readOnly.text', {
defaultMessage: 'Read only'
}),
tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save'
}),
iconType: 'glasses'
}
: undefined
);
}, []);
};

View file

@ -18,6 +18,7 @@ import { LicenseProvider } from '../context/LicenseContext';
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
import { routes } from '../components/app/Main/routeConfig';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import { useUpdateBadgeEffect } from '../components/app/Main/useUpdateBadgeEffect';
export const REACT_APP_ROOT_ID = 'react-apm-root';
@ -28,6 +29,8 @@ const MainContainer = styled.div`
`;
function App() {
useUpdateBadgeEffect();
return (
<UrlParamsProvider>
<LoadingIndicatorProvider>

View file

@ -3,15 +3,31 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Provider } from 'react-redux';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
import { App } from '../../components/app';
export function CanvasRootController(canvasStore, $scope, $element) {
export function CanvasRootController(canvasStore, $scope, $element, uiCapabilities) {
const domNode = $element[0];
// set the read-only badge when appropriate
chrome.badge.set(
uiCapabilities.canvas.save
? undefined
: {
text: i18n.translate('xpack.canvas.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n.translate('xpack.canvas.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save Canvas workpads',
}),
iconType: 'glasses',
}
);
render(
<Provider store={canvasStore}>
<App />

View file

@ -31,6 +31,7 @@ import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
import appTemplate from './templates/index.html';
import { getHomeBreadcrumbs, getWorkspaceBreadcrumbs } from './breadcrumbs';
import { getReadonlyBadge } from './badge';
import { FormattedMessage } from '@kbn/i18n/react';
import './angular-venn-simple.js';
@ -80,6 +81,7 @@ uiRoutes
.when('/home', {
template: appTemplate,
k7Breadcrumbs: getHomeBreadcrumbs,
badge: getReadonlyBadge,
resolve: {
//Copied from example found in wizard.js ( Kibana TODO - can't
// IndexPatternsProvider abstract these implementation details better?)

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export function getReadonlyBadge(i18n, uiCapabilities) {
if (uiCapabilities.graph.save) {
return null;
}
return {
text: i18n('xpack.graph.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n('xpack.graph.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save Graph workspaces',
}),
iconType: 'glasses'
};
}

View file

@ -7,28 +7,40 @@
import isEqual from 'lodash/fp/isEqual';
import React from 'react';
import { Badge } from 'ui/chrome/api/badge';
import { Breadcrumb } from 'ui/chrome/api/breadcrumbs';
interface ExternalHeaderProps {
breadcrumbs?: Breadcrumb[];
setBreadcrumbs: (breadcrumbs: Breadcrumb[]) => void;
badge: Badge | undefined;
setBadge: (badge: Badge | undefined) => void;
}
export class ExternalHeader extends React.Component<ExternalHeaderProps> {
public componentDidMount() {
this.setBreadcrumbs();
this.setBadge();
}
public componentDidUpdate(prevProps: ExternalHeaderProps) {
if (!isEqual(this.props.breadcrumbs, prevProps.breadcrumbs)) {
this.setBreadcrumbs();
}
if (!isEqual(this.props.badge, prevProps.badge)) {
this.setBadge();
}
}
public render() {
return null;
}
private setBadge = () => {
this.props.setBadge(this.props.badge);
};
private setBreadcrumbs = () => {
this.props.setBreadcrumbs(this.props.breadcrumbs || []);
};

View file

@ -6,18 +6,42 @@
import React from 'react';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { Breadcrumb } from 'ui/chrome/api/breadcrumbs';
import { WithKibanaChrome } from '../../containers/with_kibana_chrome';
import { ExternalHeader } from './external_header';
interface HeaderProps {
breadcrumbs?: Breadcrumb[];
readOnlyBadge?: boolean;
intl: InjectedIntl;
}
export const Header = ({ breadcrumbs = [] }: HeaderProps) => (
<WithKibanaChrome>
{({ setBreadcrumbs }) => (
<ExternalHeader breadcrumbs={breadcrumbs} setBreadcrumbs={setBreadcrumbs} />
)}
</WithKibanaChrome>
export const Header = injectI18n(
({ breadcrumbs = [], readOnlyBadge = false, intl }: HeaderProps) => (
<WithKibanaChrome>
{({ setBreadcrumbs, setBadge }) => (
<ExternalHeader
breadcrumbs={breadcrumbs}
setBreadcrumbs={setBreadcrumbs}
badge={
readOnlyBadge
? {
text: intl.formatMessage({
defaultMessage: 'Read only',
id: 'xpack.infra.header.badge.readOnly.text',
}),
tooltip: intl.formatMessage({
defaultMessage: 'Unable to change source configuration',
id: 'xpack.infra.header.badge.readOnly.tooltip',
}),
iconType: 'glasses',
}
: undefined
}
setBadge={setBadge}
/>
)}
</WithKibanaChrome>
)
);

View file

@ -7,6 +7,7 @@
import React from 'react';
import chrome from 'ui/chrome';
import { Badge } from 'ui/chrome/api/badge';
import { Breadcrumb } from 'ui/chrome/api/breadcrumbs';
import { RendererFunction } from '../utils/typed_react';
@ -14,6 +15,7 @@ interface WithKibanaChromeProps {
children: RendererFunction<
{
setBreadcrumbs: (newBreadcrumbs: Breadcrumb[]) => void;
setBadge: (badge: Badge | undefined) => void;
} & WithKibanaChromeState
>;
}
@ -34,6 +36,7 @@ export class WithKibanaChrome extends React.Component<
return this.props.children({
...this.state,
setBreadcrumbs: chrome.breadcrumbs.set,
setBadge: chrome.badge.set,
});
}
}

View file

@ -70,6 +70,7 @@ export const SnapshotPage = injectUICapabilities(
}),
},
]}
readOnlyBadge={!uiCapabilities.infrastructure.save}
/>
<SourceConfigurationFlyout
shouldAllowEdit={uiCapabilities.infrastructure.configureSource as boolean}

View file

@ -33,6 +33,7 @@ export const LogsPageHeader = injectUICapabilities(
}),
},
]}
readOnlyBadge={!uiCapabilities.logs.save}
/>
<DocumentTitle
title={intl.formatMessage({

View file

@ -123,7 +123,10 @@ export const MetricDetail = injectUICapabilities(
];
return (
<ColumnarPage>
<Header breadcrumbs={breadcrumbs} />
<Header
breadcrumbs={breadcrumbs}
readOnlyBadge={!uiCapabilities.infrastructure.save}
/>
<SourceConfigurationFlyout
shouldAllowEdit={
uiCapabilities.infrastructure.configureSource as boolean

View file

@ -39,7 +39,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
all: ['infrastructure-ui-source'],
read: [],
},
ui: ['show', 'configureSource'],
ui: ['show', 'configureSource', 'save'],
},
read: {
api: ['infra'],
@ -68,7 +68,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
all: ['infrastructure-ui-source'],
read: [],
},
ui: ['show', 'configureSource'],
ui: ['show', 'configureSource', 'save'],
},
read: {
api: ['infra'],

View file

@ -43,6 +43,23 @@ app.directive('mapListing', function (reactDirective) {
routes.enable();
routes
.defaults(/.*/, {
badge: (i18n, uiCapabilities) => {
if (uiCapabilities.maps.save) {
return undefined;
}
return {
text: i18n('xpack.maps.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n('xpack.maps.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save maps',
}),
iconType: 'glasses'
};
}
})
.when('/', {
template: listingTemplate,
controller($scope, gisMapSavedObjectLoader, config) {

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Badge } from 'ui/chrome/api/badge';
export type UMBadge = Badge | undefined;

View file

@ -91,6 +91,7 @@ export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter {
darkMode,
setBreadcrumbs: chrome.breadcrumbs.set,
kibanaBreadcrumbs,
setBadge: chrome.badge.set,
routerBasename,
client: graphQLClient,
renderGlobalHelpControls,

View file

@ -7,6 +7,7 @@
import { NormalizedCacheObject } from 'apollo-cache-inmemory';
import ApolloClient from 'apollo-client';
import React from 'react';
import { UMBadge } from '../badge';
import { UMBreadcrumb } from '../breadcrumbs';
import { UptimeAppProps } from '../uptime_app';
import { CreateGraphQLClient } from './adapters/framework/framework_adapter_types';
@ -17,6 +18,8 @@ export interface UMFrontendLibs {
export type UMUpdateBreadcrumbs = (breadcrumbs: UMBreadcrumb[]) => void;
export type UMUpdateBadge = (badge: UMBadge) => void;
export type UMGraphQLClient = ApolloClient<NormalizedCacheObject>; // | OtherClientType
export type BootstrapUptimeApp = (props: UptimeAppProps) => React.ReactElement<any>;

View file

@ -22,12 +22,14 @@ import {
} from '@elastic/eui';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { ApolloProvider } from 'react-apollo';
import { BrowserRouter as Router, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { capabilities } from 'ui/capabilities';
import { I18nContext } from 'ui/i18n';
import { UMBreadcrumb } from './breadcrumbs';
import { UMGraphQLClient, UMUpdateBreadcrumbs } from './lib/lib';
import { UMGraphQLClient, UMUpdateBreadcrumbs, UMUpdateBadge } from './lib/lib';
import { MonitorPage, OverviewPage } from './pages';
import { UptimeRefreshContext, UptimeSettingsContext } from './contexts';
import { UptimeDatePicker } from './components/functional/uptime_date_picker';
@ -47,6 +49,7 @@ export interface UptimeAppProps {
kibanaBreadcrumbs: UMBreadcrumb[];
routerBasename: string;
setBreadcrumbs: UMUpdateBreadcrumbs;
setBadge: UMUpdateBadge;
renderGlobalHelpControls(): void;
}
@ -58,6 +61,7 @@ const Application = (props: UptimeAppProps) => {
renderGlobalHelpControls,
routerBasename,
setBreadcrumbs,
setBadge,
} = props;
let colors: UptimeAppColors;
@ -82,6 +86,19 @@ const Application = (props: UptimeAppProps) => {
useEffect(() => {
renderGlobalHelpControls();
setBadge(
!capabilities.get().uptime.save
? {
text: i18n.translate('xpack.uptime.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n.translate('xpack.uptime.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save',
}),
iconType: 'glasses',
}
: undefined
);
}, []);
const refreshApp = () => {

View file

@ -43,7 +43,7 @@ export const initServerWithKibana = (server: KibanaServer) => {
all: [],
read: [],
},
ui: [],
ui: ['save'],
},
read: {
api: ['uptime'],

View file

@ -116,7 +116,7 @@ const kibanaFeatures: Feature[] = [
all: [],
read: [],
},
ui: ['show'],
ui: ['show', 'save'],
},
read: {
api: ['console'],
@ -177,7 +177,7 @@ const kibanaFeatures: Feature[] = [
all: ['index-pattern'],
read: [],
},
ui: ['createNew'],
ui: ['save'],
},
read: {
savedObject: {

View file

@ -324,6 +324,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:savedObjectsManagement/telemetry/read`,
`ui:${version}:savedObjectsManagement/config/read`,
`ui:${version}:dev_tools/show`,
`ui:${version}:dev_tools/save`,
'allHack:',
],
read: [
@ -415,7 +416,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:savedObjectsManagement/telemetry/edit`,
`ui:${version}:savedObjectsManagement/telemetry/read`,
`ui:${version}:savedObjectsManagement/config/read`,
`ui:${version}:indexPatterns/createNew`,
`ui:${version}:indexPatterns/save`,
'allHack:',
],
read: [
@ -579,6 +580,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:savedObjectsManagement/telemetry/read`,
`ui:${version}:savedObjectsManagement/config/read`,
`ui:${version}:apm/show`,
`ui:${version}:apm/save`,
'allHack:',
],
read: [
@ -794,6 +796,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:savedObjectsManagement/config/read`,
`ui:${version}:infrastructure/show`,
`ui:${version}:infrastructure/configureSource`,
`ui:${version}:infrastructure/save`,
'allHack:',
],
read: [
@ -850,6 +853,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:savedObjectsManagement/config/read`,
`ui:${version}:logs/show`,
`ui:${version}:logs/configureSource`,
`ui:${version}:logs/save`,
'allHack:',
],
read: [
@ -894,6 +898,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:savedObjectsManagement/telemetry/edit`,
`ui:${version}:savedObjectsManagement/telemetry/read`,
`ui:${version}:savedObjectsManagement/config/read`,
`ui:${version}:uptime/save`,
'allHack:',
],
read: [
@ -1010,6 +1015,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:catalogue/grokdebugger`,
`ui:${version}:navLinks/kibana:dev_tools`,
`ui:${version}:dev_tools/show`,
`ui:${version}:dev_tools/save`,
`ui:${version}:catalogue/advanced_settings`,
`ui:${version}:management/kibana/settings`,
`saved_object:${version}:config/create`,
@ -1027,7 +1033,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`saved_object:${version}:index-pattern/delete`,
`ui:${version}:savedObjectsManagement/index-pattern/delete`,
`ui:${version}:savedObjectsManagement/index-pattern/edit`,
`ui:${version}:indexPatterns/createNew`,
`ui:${version}:indexPatterns/save`,
`app:${version}:timelion`,
`ui:${version}:catalogue/timelion`,
`ui:${version}:navLinks/timelion`,
@ -1058,6 +1064,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:catalogue/apm`,
`ui:${version}:navLinks/apm`,
`ui:${version}:apm/show`,
`ui:${version}:apm/save`,
`api:${version}:code_user`,
`api:${version}:code_admin`,
`app:${version}:code`,
@ -1101,14 +1108,17 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`,
`ui:${version}:infrastructure/show`,
`ui:${version}:infrastructure/configureSource`,
`ui:${version}:infrastructure/save`,
`ui:${version}:catalogue/infralogging`,
`ui:${version}:navLinks/infra:logs`,
`ui:${version}:logs/show`,
`ui:${version}:logs/configureSource`,
`ui:${version}:logs/save`,
`api:${version}:uptime`,
`app:${version}:uptime`,
`ui:${version}:catalogue/uptime`,
`ui:${version}:navLinks/uptime`,
`ui:${version}:uptime/save`,
'allHack:',
],
read: [
@ -1311,6 +1321,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:catalogue/grokdebugger`,
`ui:${version}:navLinks/kibana:dev_tools`,
`ui:${version}:dev_tools/show`,
`ui:${version}:dev_tools/save`,
`ui:${version}:catalogue/advanced_settings`,
`ui:${version}:management/kibana/settings`,
`saved_object:${version}:config/create`,
@ -1328,7 +1339,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`saved_object:${version}:index-pattern/delete`,
`ui:${version}:savedObjectsManagement/index-pattern/delete`,
`ui:${version}:savedObjectsManagement/index-pattern/edit`,
`ui:${version}:indexPatterns/createNew`,
`ui:${version}:indexPatterns/save`,
`app:${version}:timelion`,
`ui:${version}:catalogue/timelion`,
`ui:${version}:navLinks/timelion`,
@ -1359,6 +1370,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:catalogue/apm`,
`ui:${version}:navLinks/apm`,
`ui:${version}:apm/show`,
`ui:${version}:apm/save`,
`api:${version}:code_user`,
`api:${version}:code_admin`,
`app:${version}:code`,
@ -1402,14 +1414,17 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
`ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`,
`ui:${version}:infrastructure/show`,
`ui:${version}:infrastructure/configureSource`,
`ui:${version}:infrastructure/save`,
`ui:${version}:catalogue/infralogging`,
`ui:${version}:navLinks/infra:logs`,
`ui:${version}:logs/show`,
`ui:${version}:logs/configureSource`,
`ui:${version}:logs/save`,
`api:${version}:uptime`,
`app:${version}:uptime`,
`ui:${version}:catalogue/uptime`,
`ui:${version}:navLinks/uptime`,
`ui:${version}:uptime/save`,
'allHack:',
],
read: [

View file

@ -14,6 +14,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']);
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
const globalNav = getService('globalNav');
describe('security feature controls', () => {
before(async () => {
@ -81,6 +82,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const advancedSetting = await PageObjects.settings.getAdvancedSettings('dateFormat:tz');
expect(advancedSetting).to.be('America/Phoenix');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
describe('global advanced_settings read-only privileges', () => {
@ -133,6 +138,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.settings.clickKibanaSettings();
await PageObjects.settings.expectDisabledAdvancedSetting('dateFormat:tz');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
describe('no advanced_settings privileges', () => {

View file

@ -14,6 +14,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const PageObjects = getPageObjects(['common', 'error', 'security']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
const globalNav = getService('globalNav');
describe('security', () => {
before(async () => {
@ -71,6 +72,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.common.navigateToApp('apm');
await testSubjects.existOrFail('apmMainContainer', 10000);
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
describe('global apm read-only privileges', () => {
@ -116,6 +121,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.common.navigateToApp('apm');
await testSubjects.existOrFail('apmMainContainer', 10000);
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
describe('no apm privileges', () => {

View file

@ -13,6 +13,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']);
const find = getService('find');
const appsMenu = getService('appsMenu');
const globalNav = getService('globalNav');
describe('security feature controls', () => {
before(async () => {
@ -79,6 +80,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.canvas.expectCreateWorkpadButtonEnabled();
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
it(`allows a workpad to be created`, async () => {
await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', {
ensureCurrentUrl: true,
@ -153,6 +158,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.canvas.expectCreateWorkpadButtonDisabled();
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
it(`does not allow a workpad to be created`, async () => {
await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', {
ensureCurrentUrl: false,

View file

@ -20,6 +20,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const appsMenu = getService('appsMenu');
const panelActions = getService('dashboardPanelActions');
const testSubjects = getService('testSubjects');
const globalNav = getService('globalNav');
describe('security', () => {
before(async () => {
@ -94,6 +95,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('newItemButton');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
it(`create new dashboard shows addNew button`, async () => {
await PageObjects.common.navigateToActualUrl(
'kibana',
@ -227,6 +232,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.missingOrFail('newItemButton');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
it(`create new dashboard redirects to the home page`, async () => {
await PageObjects.common.navigateToActualUrl(
'kibana',

View file

@ -15,6 +15,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
const grokDebugger = getService('grokDebugger');
const globalNav = getService('globalNav');
describe('security', () => {
before(async () => {
@ -70,19 +71,46 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
]);
});
it(`can navigate to console`, async () => {
await PageObjects.common.navigateToApp('console');
await testSubjects.existOrFail('console');
describe('console', () => {
before(async () => {
await PageObjects.common.navigateToApp('console');
});
it(`can navigate to console`, async () => {
await testSubjects.existOrFail('console');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
it(`can navigate to search profiler`, async () => {
await PageObjects.common.navigateToApp('searchProfiler');
await testSubjects.existOrFail('searchProfiler');
describe('search profiler', () => {
before(async () => {
await PageObjects.common.navigateToApp('searchProfiler');
});
it(`can navigate to search profiler`, async () => {
await testSubjects.existOrFail('searchProfiler');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
it(`can navigate to grok debugger`, async () => {
await PageObjects.common.navigateToApp('grokDebugger');
await grokDebugger.assertExists();
describe('grok debugger', () => {
before(async () => {
await PageObjects.common.navigateToApp('grokDebugger');
});
it(`can navigate to grok debugger`, async () => {
await grokDebugger.assertExists();
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
});
@ -126,19 +154,46 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
expect(navLinks).to.eql(['Dev Tools', 'Management']);
});
it(`can navigate to console`, async () => {
await PageObjects.common.navigateToApp('console');
await testSubjects.existOrFail('console');
describe('console', () => {
before(async () => {
await PageObjects.common.navigateToApp('console');
});
it(`can navigate to console`, async () => {
await testSubjects.existOrFail('console');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
it(`can navigate to search profiler`, async () => {
await PageObjects.common.navigateToApp('searchProfiler');
await testSubjects.existOrFail('searchProfiler');
describe('search profiler', () => {
before(async () => {
await PageObjects.common.navigateToApp('searchProfiler');
});
it(`can navigate to search profiler`, async () => {
await testSubjects.existOrFail('searchProfiler');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
it(`can navigate to grok debugger`, async () => {
await PageObjects.common.navigateToApp('grokDebugger');
await grokDebugger.assertExists();
describe('grok debugger', () => {
before(async () => {
await PageObjects.common.navigateToApp('grokDebugger');
});
it(`can navigate to grok debugger`, async () => {
await grokDebugger.assertExists();
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
});

View file

@ -11,6 +11,7 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../../types/provider
export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) {
const esArchiver = getService('esArchiver');
const security: SecurityService = getService('security');
const globalNav = getService('globalNav');
const PageObjects = getPageObjects([
'common',
'discover',
@ -93,6 +94,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('discoverSaveButton', 20000);
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
it('Permalinks shows create short-url button', async () => {
await PageObjects.share.openShareMenuItem('Permalinks');
await PageObjects.share.createShortUrlExistOrFail();
@ -148,6 +153,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.missingOrFail('discoverSaveButton');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
it(`doesn't show visualize button`, async () => {
await PageObjects.common.navigateToApp('discover');
await setDiscoverTimeRange();

View file

@ -14,6 +14,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
const globalNav = getService('globalNav');
describe('security', () => {
before(async () => {
@ -80,6 +81,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.common.navigateToApp('graph');
await testSubjects.existOrFail('graphDeleteButton');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
describe('global graph read-only privileges', () => {
@ -136,6 +141,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('graphOpenButton');
await testSubjects.missingOrFail('graphDeleteButton');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
describe('no graph privileges', () => {

View file

@ -14,6 +14,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const PageObjects = getPageObjects(['common', 'settings', 'security']);
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
const globalNav = getService('globalNav');
describe('security', () => {
before(async () => {
@ -79,6 +80,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.settings.clickKibanaIndexPatterns();
await testSubjects.existOrFail('createIndexPatternButton');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
describe('global index_patterns read-only privileges', () => {
@ -132,6 +137,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('indexPatternTable');
await testSubjects.missingOrFail('createIndexPatternButton');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
describe('no index_patterns privileges', () => {

View file

@ -15,6 +15,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const PageObjects = getPageObjects(['common', 'infraHome', 'security']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
const globalNav = getService('globalNav');
describe('infrastructure security', () => {
describe('global infrastructure all privileges', () => {
@ -74,6 +75,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('infrastructureViewSetupInstructionsButton');
await testSubjects.existOrFail('configureSourceButton');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
describe('infrastructure landing page with data', () => {
@ -107,6 +112,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.missingOrFail('viewApmTracesContextMenuItem');
});
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
it(`metrics page is visible`, async () => {
@ -179,6 +188,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('infrastructureViewSetupInstructionsButton');
await testSubjects.missingOrFail('configureSourceButton');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
describe('infrastructure landing page with data', () => {
@ -212,6 +225,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.missingOrFail('viewApmTracesContextMenuItem');
});
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
it(`metrics page is visible`, async () => {

View file

@ -14,6 +14,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const PageObjects = getPageObjects(['common', 'infraHome', 'security']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
const globalNav = getService('globalNav');
describe('logs security', () => {
before(async () => {
@ -73,6 +74,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('logsViewSetupInstructionsButton');
await testSubjects.existOrFail('configureSourceButton');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
});
@ -134,6 +139,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('logsViewSetupInstructionsButton');
await testSubjects.missingOrFail('configureSourceButton');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
});

View file

@ -14,6 +14,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
const find = getService('find');
const globalNav = getService('globalNav');
const getMessageText = async () => await (await find.byCssSelector('body>pre')).getVisibleText();
@ -80,6 +81,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
it(`allows a map to be deleted`, async () => {
await PageObjects.maps.deleteSavedMaps('my test map');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
describe('global maps read-only privileges', () => {
@ -135,6 +140,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.missingOrFail('checkboxSelectAll');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
describe('existing map', () => {
before(async () => {
await PageObjects.maps.loadSavedMap('document example');

View file

@ -13,6 +13,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const PageObjects = getPageObjects(['common', 'timelion', 'header', 'security', 'spaceSelector']);
const find = getService('find');
const appsMenu = getService('appsMenu');
const globalNav = getService('globalNav');
describe('feature controls security', () => {
before(async () => {
@ -70,6 +71,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.common.navigateToApp('timelion');
await PageObjects.timelion.saveTimelionSheet();
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
describe('global timelion read-only privileges', () => {
@ -120,6 +125,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.common.navigateToApp('timelion');
await PageObjects.timelion.expectMissingWriteControls();
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
describe('no timelion privileges', () => {

View file

@ -14,6 +14,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
const globalNav = getService('globalNav');
describe('security', () => {
before(async () => {
@ -75,6 +76,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.common.navigateToApp('uptime');
await testSubjects.existOrFail('uptimeApp', 10000);
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
});
describe('global uptime read-only privileges', () => {
@ -124,6 +129,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await PageObjects.common.navigateToApp('uptime');
await testSubjects.existOrFail('uptimeApp', 10000);
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
});
describe('no uptime privileges', () => {

View file

@ -21,6 +21,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
]);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
const globalNav = getService('globalNav');
describe('feature controls security', () => {
before(async () => {
@ -84,6 +85,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('newItemButton');
});
it(`doesn't show read-only badge`, async () => {
await globalNav.badgeMissingOrFail();
});
it(`can view existing Visualization`, async () => {
await PageObjects.common.navigateToActualUrl('kibana', '/visualize/edit/i-exist', {
ensureCurrentUrl: false,
@ -161,6 +166,10 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
await testSubjects.existOrFail('newItemButton');
});
it(`shows read-only badge`, async () => {
await globalNav.badgeExistsOrFail('Read only');
});
it(`can view existing Visualization`, async () => {
await PageObjects.common.navigateToActualUrl('visualize', '/visualize/edit/i-exist', {
ensureCurrentUrl: false,