mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* 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:
parent
b037c4b5af
commit
7943f9895d
67 changed files with 965 additions and 89 deletions
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [iconType](./kibana-plugin-public.chromebadge.icontype.md)
|
||||
|
||||
## ChromeBadge.iconType property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
iconType?: IconType;
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [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> | |
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [text](./kibana-plugin-public.chromebadge.text.md)
|
||||
|
||||
## ChromeBadge.text property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
text: string;
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [tooltip](./kibana-plugin-public.chromebadge.tooltip.md)
|
||||
|
||||
## ChromeBadge.tooltip property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
tooltip: string;
|
||||
```
|
|
@ -1,45 +1,46 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index) > [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) > [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) | |
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
export {
|
||||
ChromeBadge,
|
||||
ChromeBreadcrumb,
|
||||
ChromeService,
|
||||
ChromeSetup,
|
||||
|
|
|
@ -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';
|
||||
|
@ -90,6 +96,7 @@ export {
|
|||
Capabilities,
|
||||
CapabilitiesStart,
|
||||
ChromeSetup,
|
||||
ChromeBadge,
|
||||
ChromeBreadcrumb,
|
||||
ChromeBrand,
|
||||
ChromeHelpExtension,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -113,7 +113,8 @@ export default function (kibana) {
|
|||
),
|
||||
uiCapabilities: {
|
||||
dev_tools: {
|
||||
show: true
|
||||
show: true,
|
||||
save: true,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -172,7 +172,7 @@ export default function (kibana) {
|
|||
save: true
|
||||
},
|
||||
indexPatterns: {
|
||||
createNew: true,
|
||||
save: true,
|
||||
},
|
||||
savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({
|
||||
...acc,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -64,7 +64,7 @@ class CreateButtonComponent extends Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!uiCapabilities.indexPatterns.createNew) {
|
||||
if (!uiCapabilities.indexPatterns.save) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
64
src/legacy/ui/public/chrome/api/badge.test.ts
Normal file
64
src/legacy/ui/public/chrome/api/badge.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
59
src/legacy/ui/public/chrome/api/badge.ts
Normal file
59
src/legacy/ui/public/chrome/api/badge.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -28,3 +28,8 @@
|
|||
.chrHeaderHelpMenu__version {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.chrHeaderBadge__wrapper {
|
||||
align-self: center;
|
||||
margin-right: $euiSize;
|
||||
}
|
||||
|
|
|
@ -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$} />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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$(),
|
||||
|
|
3
src/legacy/ui/public/chrome/index.d.ts
vendored
3
src/legacy/ui/public/chrome/index.d.ts
vendored
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ export function apm(kibana: any) {
|
|||
all: [],
|
||||
read: []
|
||||
},
|
||||
ui: ['show']
|
||||
ui: ['show', 'save']
|
||||
},
|
||||
read: {
|
||||
api: ['apm'],
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}, []);
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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?)
|
||||
|
|
22
x-pack/plugins/graph/public/badge.js
Normal file
22
x-pack/plugins/graph/public/badge.js
Normal 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'
|
||||
};
|
||||
}
|
|
@ -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 || []);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ export const SnapshotPage = injectUICapabilities(
|
|||
}),
|
||||
},
|
||||
]}
|
||||
readOnlyBadge={!uiCapabilities.infrastructure.save}
|
||||
/>
|
||||
<SourceConfigurationFlyout
|
||||
shouldAllowEdit={uiCapabilities.infrastructure.configureSource as boolean}
|
||||
|
|
|
@ -33,6 +33,7 @@ export const LogsPageHeader = injectUICapabilities(
|
|||
}),
|
||||
},
|
||||
]}
|
||||
readOnlyBadge={!uiCapabilities.logs.save}
|
||||
/>
|
||||
<DocumentTitle
|
||||
title={intl.formatMessage({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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) {
|
||||
|
|
8
x-pack/plugins/uptime/public/badge.ts
Normal file
8
x-pack/plugins/uptime/public/badge.ts
Normal 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;
|
|
@ -91,6 +91,7 @@ export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter {
|
|||
darkMode,
|
||||
setBreadcrumbs: chrome.breadcrumbs.set,
|
||||
kibanaBreadcrumbs,
|
||||
setBadge: chrome.badge.set,
|
||||
routerBasename,
|
||||
client: graphQLClient,
|
||||
renderGlobalHelpControls,
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -43,7 +43,7 @@ export const initServerWithKibana = (server: KibanaServer) => {
|
|||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
ui: ['save'],
|
||||
},
|
||||
read: {
|
||||
api: ['uptime'],
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue