[7.x] Grouped features for space management (#74151) (#77925)

* Grouped features for space management

* Apply suggestions from code review

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>

* Address PR Feedback

* docs changes

* updating types/docs

* update APM feature name

* Reintroduce extraAction following EUI update

* change ordering of infra features, and render callout for management category

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Larry Gregory 2020-09-18 14:17:40 -04:00 committed by GitHub
parent 91d0a3b665
commit cdbf4ffb8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 965 additions and 313 deletions

View file

@ -38,6 +38,12 @@ Registering a feature consists of the following fields. For more information, co
|`"Sample Feature"`
|A human readable name for your feature.
|`category` (required)
|{kib-repo}blob/{branch}/src/core/types/app_category.ts[`AppCategory`]
|`DEFAULT_APP_CATEGORIES.kibana`
|The `AppCategory` which best represents your feature. Used to organize the display
of features within the management screens.
|`app` (required)
|`string[]`
|`["sample_app", "kibana"]`
@ -96,6 +102,7 @@ public setup(core, { features }) {
name: 'Canvas',
icon: 'canvasApp',
navLinkId: 'canvas',
category: DEFAULT_APP_CATEGORIES.kibana,
app: ['canvas', 'kibana'],
catalogue: ['canvas'],
privileges: {
@ -155,6 +162,7 @@ public setup(core, { features }) {
}),
icon: 'devToolsApp',
navLinkId: 'dev_tools',
category: DEFAULT_APP_CATEGORIES.management,
app: ['kibana'],
catalogue: ['console', 'searchprofiler', 'grokdebugger'],
privileges: {
@ -217,6 +225,7 @@ public setup(core, { features }) {
order: 100,
icon: 'discoverApp',
navLinkId: 'discover',
category: DEFAULT_APP_CATEGORIES.kibana,
app: ['kibana'],
catalogue: ['discover'],
privileges: {

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [AppCategory](./kibana-plugin-core-server.appcategory.md) &gt; [ariaLabel](./kibana-plugin-core-server.appcategory.arialabel.md)
## AppCategory.ariaLabel property
If the visual label isn't appropriate for screen readers, can override it here
<b>Signature:</b>
```typescript
ariaLabel?: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [AppCategory](./kibana-plugin-core-server.appcategory.md) &gt; [euiIconType](./kibana-plugin-core-server.appcategory.euiicontype.md)
## AppCategory.euiIconType property
Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined
<b>Signature:</b>
```typescript
euiIconType?: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [AppCategory](./kibana-plugin-core-server.appcategory.md) &gt; [id](./kibana-plugin-core-server.appcategory.id.md)
## AppCategory.id property
Unique identifier for the categories
<b>Signature:</b>
```typescript
id: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [AppCategory](./kibana-plugin-core-server.appcategory.md) &gt; [label](./kibana-plugin-core-server.appcategory.label.md)
## AppCategory.label property
Label used for category name. Also used as aria-label if one isn't set.
<b>Signature:</b>
```typescript
label: string;
```

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [AppCategory](./kibana-plugin-core-server.appcategory.md)
## AppCategory interface
A category definition for nav links to know where to sort them in the left hand nav
<b>Signature:</b>
```typescript
export interface AppCategory
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [ariaLabel](./kibana-plugin-core-server.appcategory.arialabel.md) | <code>string</code> | If the visual label isn't appropriate for screen readers, can override it here |
| [euiIconType](./kibana-plugin-core-server.appcategory.euiicontype.md) | <code>string</code> | Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined |
| [id](./kibana-plugin-core-server.appcategory.id.md) | <code>string</code> | Unique identifier for the categories |
| [label](./kibana-plugin-core-server.appcategory.label.md) | <code>string</code> | Label used for category name. Also used as aria-label if one isn't set. |
| [order](./kibana-plugin-core-server.appcategory.order.md) | <code>number</code> | The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [AppCategory](./kibana-plugin-core-server.appcategory.md) &gt; [order](./kibana-plugin-core-server.appcategory.order.md)
## AppCategory.order property
The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000)
<b>Signature:</b>
```typescript
order?: number;
```

View file

@ -50,6 +50,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| Interface | Description |
| --- | --- |
| [AppCategory](./kibana-plugin-core-server.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav |
| [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) | |
| [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) | |
| [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) | Event to audit. |

View file

@ -19,6 +19,7 @@
import { Plugin, CoreSetup } from 'kibana/server';
import { i18n } from '@kbn/i18n';
import { DEFAULT_APP_CATEGORIES } from '../../../src/core/server';
import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plugins/features/server';
@ -47,6 +48,7 @@ export class AlertingExamplePlugin implements Plugin<void, void, AlertingExample
management: {
insightsAndAlerting: ['triggersActions'],
},
category: DEFAULT_APP_CATEGORIES.management,
alerting: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
privileges: {
all: {

View file

@ -171,6 +171,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
Object {
"baseUrl": "/",
"category": Object {
"euiIconType": "managementApp",
"id": "management",
"label": "Management",
"order": 5000,
@ -1606,6 +1607,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
</EuiCollapsibleNavGroup>
<EuiCollapsibleNavGroup
data-test-subj="collapsibleNavGroup-management"
iconType="managementApp"
initialIsOpen={true}
isCollapsible={true}
key="management"
@ -1621,6 +1623,14 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
gutterSize="m"
responsive={false}
>
<EuiFlexItem
grow={false}
>
<EuiIcon
size="l"
type="managementApp"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle
size="xxs"
@ -1686,6 +1696,23 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
<div
className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<EuiFlexItem
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiIcon
size="l"
type="managementApp"
>
<div
data-euiicon-type="managementApp"
size="l"
/>
</EuiIcon>
</div>
</EuiFlexItem>
<EuiFlexItem>
<div
className="euiFlexItem"

View file

@ -446,37 +446,7 @@ export class CoreSystem {
}
// @internal (undocumented)
export const DEFAULT_APP_CATEGORIES: Readonly<{
kibana: {
id: string;
label: string;
euiIconType: string;
order: number;
};
enterpriseSearch: {
id: string;
label: string;
order: number;
euiIconType: string;
};
observability: {
id: string;
label: string;
euiIconType: string;
order: number;
};
security: {
id: string;
label: string;
order: number;
euiIconType: string;
};
management: {
id: string;
label: string;
order: number;
};
}>;
export const DEFAULT_APP_CATEGORIES: Record<string, AppCategory>;
// @public (undocumented)
export interface DocLinksStart {

View file

@ -323,6 +323,7 @@ export {
MetricsServiceStart,
} from './metrics';
export { AppCategory } from '../types';
export { DEFAULT_APP_CATEGORIES } from '../utils';
export {

View file

@ -164,6 +164,15 @@ import { UpdateDocumentByQueryParams } from 'elasticsearch';
import { UpdateDocumentParams } from 'elasticsearch';
import { Url } from 'url';
// @public
export interface AppCategory {
ariaLabel?: string;
euiIconType?: string;
id: string;
label: string;
order?: number;
}
// Warning: (ae-forgotten-export) The symbol "ConsoleAppenderConfig" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "FileAppenderConfig" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "LegacyAppenderConfig" needs to be exported by the entry point index.d.ts
@ -484,37 +493,7 @@ export interface CustomHttpResponseOptions<T extends HttpResponsePayload | Respo
}
// @internal (undocumented)
export const DEFAULT_APP_CATEGORIES: Readonly<{
kibana: {
id: string;
label: string;
euiIconType: string;
order: number;
};
enterpriseSearch: {
id: string;
label: string;
order: number;
euiIconType: string;
};
observability: {
id: string;
label: string;
euiIconType: string;
order: number;
};
security: {
id: string;
label: string;
order: number;
euiIconType: string;
};
management: {
id: string;
label: string;
order: number;
};
}>;
export const DEFAULT_APP_CATEGORIES: Record<string, AppCategory>;
// @public (undocumented)
export interface DeleteDocumentResponse {

View file

@ -18,9 +18,10 @@
*/
import { i18n } from '@kbn/i18n';
import { AppCategory } from '../types';
/** @internal */
export const DEFAULT_APP_CATEGORIES = Object.freeze({
export const DEFAULT_APP_CATEGORIES: Record<string, AppCategory> = Object.freeze({
kibana: {
id: 'kibana',
label: i18n.translate('core.ui.kibanaNavList.label', {
@ -59,5 +60,6 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({
defaultMessage: 'Management',
}),
order: 5000,
euiIconType: 'managementApp',
},
});

View file

@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
export const ACTIONS_FEATURE = {
id: 'actions',
@ -14,6 +15,7 @@ export const ACTIONS_FEATURE = {
}),
icon: 'bell',
navLinkId: 'actions',
category: DEFAULT_APP_CATEGORIES.management,
app: [],
management: {
insightsAndAlerting: ['triggersActions'],

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type';
import { BUILT_IN_ALERTS_FEATURE_ID } from '../common';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
export const BUILT_IN_ALERTS_FEATURE = {
id: BUILT_IN_ALERTS_FEATURE_ID,
@ -15,6 +16,7 @@ export const BUILT_IN_ALERTS_FEATURE = {
}),
icon: 'bell',
app: [],
category: DEFAULT_APP_CATEGORIES.management,
management: {
insightsAndAlerting: ['triggersActions'],
},

View file

@ -44,6 +44,7 @@ function mockFeature(appName: string, typeName?: string) {
id: appName,
name: appName,
app: [],
category: { id: 'foo', label: 'foo' },
...(typeName
? {
alerting: [typeName],
@ -87,6 +88,7 @@ function mockFeatureWithSubFeature(appName: string, typeName: string) {
id: appName,
name: appName,
app: [],
category: { id: 'foo', label: 'foo' },
...(typeName
? {
alerting: [typeName],

View file

@ -164,6 +164,7 @@ function mockFeatures() {
id: 'appName',
name: 'appName',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { LicenseType } from '../../licensing/common/types';
import { AlertType } from '../common/alert_types';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import {
LicensingPluginSetup,
LicensingRequestHandlerContext,
@ -15,9 +16,10 @@ import {
export const APM_FEATURE = {
id: 'apm',
name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', {
defaultMessage: 'APM',
defaultMessage: 'APM and Client Side Monitoring',
}),
order: 900,
category: DEFAULT_APP_CATEGORIES.observability,
icon: 'apmApp',
navLinkId: 'apm',
app: ['apm', 'csm', 'kibana'],

View file

@ -10,6 +10,7 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { HomeServerPluginSetup } from 'src/plugins/home/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { initRoutes } from './routes';
import { registerCanvasUsageCollector } from './collectors';
@ -40,7 +41,8 @@ export class CanvasPlugin implements Plugin {
plugins.features.registerKibanaFeature({
id: 'canvas',
name: 'Canvas',
order: 400,
order: 300,
category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'canvasApp',
navLinkId: 'canvas',
app: ['canvas', 'kibana'],

View file

@ -16,6 +16,7 @@ import {
KibanaRequest,
} from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { SecurityPluginSetup } from '../../security/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
@ -82,6 +83,7 @@ export class EnterpriseSearchPlugin implements Plugin {
id: ENTERPRISE_SEARCH_PLUGIN.ID,
name: ENTERPRISE_SEARCH_PLUGIN.NAME,
order: 0,
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
icon: 'logoEnterpriseSearch',
app: [
'kibana',

View file

@ -5,6 +5,7 @@
*/
import { RecursiveReadonly } from '@kbn/utility-types';
import { AppCategory } from 'src/core/types';
import { FeatureKibanaPrivileges } from './feature_kibana_privileges';
import { SubFeatureConfig, SubFeature as KibanaSubFeature } from './sub_feature';
import { ReservedKibanaPrivilege } from './reserved_kibana_privilege';
@ -29,6 +30,13 @@ export interface KibanaFeatureConfig {
*/
name: string;
/**
* The category for this feature.
* This will be used to organize the list of features for display within the
* Spaces and Roles management screens.
*/
category: AppCategory;
/**
* An ordinal used to sort features relative to one another for display.
*/
@ -158,6 +166,10 @@ export class KibanaFeature {
return this.config.order;
}
public get category() {
return this.config.category;
}
public get navLinkId() {
return this.config.navLinkId;
}

View file

@ -14,6 +14,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
};
@ -35,6 +36,7 @@ describe('FeatureRegistry', () => {
icon: 'addDataApp',
navLinkId: 'someNavLink',
app: ['app1'],
category: { id: 'foo', label: 'foo' },
validLicenses: ['standard', 'basic', 'gold', 'platinum'],
catalogue: ['foo'],
management: {
@ -143,11 +145,64 @@ describe('FeatureRegistry', () => {
expect(result[0].toRaw()).toEqual(feature);
});
describe('category', () => {
it('is required', () => {
const feature: KibanaFeatureConfig = {
id: 'test-feature',
name: 'Test Feature',
app: [],
privileges: null,
} as any;
const featureRegistry = new FeatureRegistry();
expect(() =>
featureRegistry.registerKibanaFeature(feature)
).toThrowErrorMatchingInlineSnapshot(
`"child \\"category\\" fails because [\\"category\\" is required]"`
);
});
it('must have an id', () => {
const feature: KibanaFeatureConfig = {
id: 'test-feature',
name: 'Test Feature',
app: [],
privileges: null,
category: { label: 'foo' },
} as any;
const featureRegistry = new FeatureRegistry();
expect(() =>
featureRegistry.registerKibanaFeature(feature)
).toThrowErrorMatchingInlineSnapshot(
`"child \\"category\\" fails because [child \\"id\\" fails because [\\"id\\" is required]]"`
);
});
it('must have a label', () => {
const feature: KibanaFeatureConfig = {
id: 'test-feature',
name: 'Test Feature',
app: [],
privileges: null,
category: { id: 'foo' },
} as any;
const featureRegistry = new FeatureRegistry();
expect(() =>
featureRegistry.registerKibanaFeature(feature)
).toThrowErrorMatchingInlineSnapshot(
`"child \\"category\\" fails because [child \\"label\\" fails because [\\"label\\" is required]]"`
);
});
});
it(`requires a value for privileges`, () => {
const feature: KibanaFeatureConfig = {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
} as any;
const featureRegistry = new FeatureRegistry();
@ -163,6 +218,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
subFeatures: [
{
@ -201,6 +257,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
ui: [],
@ -235,6 +292,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
ui: [],
@ -271,6 +329,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@ -303,6 +362,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
ui: [],
@ -340,6 +400,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
};
@ -347,6 +408,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Duplicate Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
};
@ -367,6 +429,7 @@ describe('FeatureRegistry', () => {
name: 'some feature',
navLinkId: prohibitedChars,
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
})
).toThrowErrorMatchingSnapshot();
@ -382,6 +445,7 @@ describe('FeatureRegistry', () => {
kibana: [prohibitedChars],
},
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
})
).toThrowErrorMatchingSnapshot();
@ -395,6 +459,7 @@ describe('FeatureRegistry', () => {
name: 'some feature',
catalogue: [prohibitedChars],
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
})
).toThrowErrorMatchingSnapshot();
@ -409,6 +474,7 @@ describe('FeatureRegistry', () => {
id: prohibitedId,
name: 'some feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
})
).toThrowErrorMatchingSnapshot();
@ -420,6 +486,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['app1', 'app2'],
category: { id: 'foo', label: 'foo' },
privileges: {
foo: {
name: 'Foo',
@ -447,6 +514,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['bar'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -481,6 +549,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['foo', 'bar', 'baz'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -538,6 +607,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['bar'],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'something',
@ -571,6 +641,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['foo', 'bar', 'baz'],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'something',
@ -604,6 +675,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
privileges: {
all: {
@ -641,6 +713,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['foo', 'bar', 'baz'],
privileges: {
all: {
@ -701,6 +774,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
privileges: null,
reserved: {
@ -736,6 +810,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['foo', 'bar', 'baz'],
privileges: null,
reserved: {
@ -771,6 +846,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
alerting: ['bar'],
privileges: {
all: {
@ -811,6 +887,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
alerting: ['foo', 'bar', 'baz'],
privileges: {
all: {
@ -871,6 +948,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
alerting: ['bar'],
privileges: null,
reserved: {
@ -906,6 +984,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
alerting: ['foo', 'bar', 'baz'],
privileges: null,
reserved: {
@ -941,6 +1020,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
management: {
kibana: ['hey'],
@ -987,6 +1067,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
management: {
kibana: ['hey'],
@ -1060,6 +1141,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
management: {
kibana: ['hey'],
@ -1101,6 +1183,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
management: {
kibana: ['hey', 'hey-there'],
@ -1142,6 +1225,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'my reserved privileges',
@ -1184,6 +1268,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'my reserved privileges',
@ -1216,12 +1301,14 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
};
const feature2: KibanaFeatureConfig = {
id: 'test-feature-2',
name: 'Test Feature 2',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
};
@ -1346,6 +1433,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
};
@ -1371,6 +1459,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
};

View file

@ -28,6 +28,14 @@ const managementSchema = Joi.object().pattern(
const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex));
const alertingSchema = Joi.array().items(Joi.string());
const appCategorySchema = Joi.object({
id: Joi.string().required(),
label: Joi.string().required(),
ariaLabel: Joi.string(),
euiIconType: Joi.string(),
order: Joi.number(),
}).required();
const kibanaPrivilegeSchema = Joi.object({
excludeFromBasePrivileges: Joi.boolean(),
management: managementSchema,
@ -80,6 +88,7 @@ const kibanaFeatureSchema = Joi.object({
.invalid(...prohibitedFeatureIds)
.required(),
name: Joi.string().required(),
category: appCategorySchema,
order: Joi.number(),
excludeFromBasePrivileges: Joi.boolean(),
validLicenses: Joi.array().items(

View file

@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import { KibanaFeatureConfig } from '../common';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
export interface BuildOSSFeaturesParams {
savedObjectTypes: string[];
@ -19,6 +20,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Discover',
}),
order: 100,
category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'discoverApp',
navLinkId: 'discover',
app: ['discover', 'kibana'],
@ -78,7 +80,8 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
name: i18n.translate('xpack.features.visualizeFeatureName', {
defaultMessage: 'Visualize',
}),
order: 200,
order: 700,
category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'visualizeApp',
navLinkId: 'visualize',
app: ['visualize', 'lens', 'kibana'],
@ -138,7 +141,8 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
name: i18n.translate('xpack.features.dashboardFeatureName', {
defaultMessage: 'Dashboard',
}),
order: 300,
order: 200,
category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'dashboardApp',
navLinkId: 'dashboards',
app: ['dashboards', 'kibana'],
@ -217,6 +221,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Dev Tools',
}),
order: 1300,
category: DEFAULT_APP_CATEGORIES.management,
icon: 'devToolsApp',
navLinkId: 'dev_tools',
app: ['dev_tools', 'kibana'],
@ -254,6 +259,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Advanced Settings',
}),
order: 1500,
category: DEFAULT_APP_CATEGORIES.management,
icon: 'advancedSettingsApp',
app: ['kibana'],
catalogue: ['advanced_settings'],
@ -293,6 +299,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Index Pattern Management',
}),
order: 1600,
category: DEFAULT_APP_CATEGORIES.management,
icon: 'indexPatternApp',
app: ['kibana'],
catalogue: ['indexPatterns'],
@ -332,6 +339,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Saved Objects Management',
}),
order: 1700,
category: DEFAULT_APP_CATEGORIES.management,
icon: 'savedObjectsApp',
app: ['kibana'],
catalogue: ['saved_objects'],
@ -375,6 +383,7 @@ const timelionFeature: KibanaFeatureConfig = {
id: 'timelion',
name: 'Timelion',
order: 350,
category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'timelionApp',
navLinkId: 'timelion',
app: ['timelion', 'kibana'],

View file

@ -35,6 +35,7 @@ describe('Features Plugin', () => {
id: 'baz',
name: 'baz',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
});
@ -63,6 +64,7 @@ describe('Features Plugin', () => {
id: 'baz',
name: 'baz',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
});

View file

@ -28,6 +28,7 @@ describe('GET /api/features', () => {
id: 'feature_1',
name: 'Feature 1',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
});
@ -36,6 +37,7 @@ describe('GET /api/features', () => {
name: 'Feature 2',
order: 2,
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
});
@ -44,6 +46,7 @@ describe('GET /api/features', () => {
name: 'Feature 2',
order: 1,
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
});
@ -51,6 +54,7 @@ describe('GET /api/features', () => {
id: 'licensed_feature',
name: 'Licensed Feature',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
validLicenses: ['gold'],
privileges: null,
});

View file

@ -46,6 +46,7 @@ describe('populateUICapabilities', () => {
id: 'newFeature',
name: 'my new feature',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(),
read: createKibanaFeaturePrivilege(),
@ -93,6 +94,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(),
@ -146,6 +148,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
catalogue: ['anotherFooEntry', 'anotherBarEntry'],
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
@ -215,6 +218,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4', 'capability5']),
@ -245,6 +249,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: '',
@ -289,6 +294,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
@ -360,6 +366,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
@ -369,6 +376,7 @@ describe('populateUICapabilities', () => {
id: 'anotherNewFeature',
name: 'another new feature',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
@ -379,6 +387,7 @@ describe('populateUICapabilities', () => {
name: 'yet another new feature',
navLinkId: 'yetAnotherNavLink',
app: ['bar-app'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['something1', 'something2', 'something3']),

View file

@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import { Plugin, CoreSetup, CoreStart } from 'src/core/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
import { LicenseState } from './lib/license_state';
import { registerSearchRoute } from './routes/search';
@ -46,7 +47,8 @@ export class GraphPlugin implements Plugin {
name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', {
defaultMessage: 'Graph',
}),
order: 1200,
order: 600,
category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'graphApp',
navLinkId: 'graph',
app: ['graph', 'kibana'],

View file

@ -8,13 +8,15 @@ import { i18n } from '@kbn/i18n';
import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../common/alerting/logs/types';
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/inventory_metric_threshold/types';
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
export const METRICS_FEATURE = {
id: 'infrastructure',
name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', {
defaultMessage: 'Metrics',
}),
order: 700,
order: 800,
category: DEFAULT_APP_CATEGORIES.observability,
icon: 'metricsApp',
navLinkId: 'metrics',
app: ['infra', 'metrics', 'kibana'],
@ -64,7 +66,8 @@ export const LOGS_FEATURE = {
name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', {
defaultMessage: 'Logs',
}),
order: 800,
order: 700,
category: DEFAULT_APP_CATEGORIES.observability,
icon: 'logsApp',
navLinkId: 'logs',
app: ['infra', 'logs', 'kibana'],

View file

@ -16,6 +16,7 @@ import {
SavedObjectsClientContract,
} from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { LicensingPluginSetup, ILicense } from '../../licensing/server';
import {
EncryptedSavedObjectsPluginStart,
@ -181,6 +182,7 @@ export class IngestManagerPlugin
id: PLUGIN_ID,
name: 'Ingest Manager',
icon: 'savedObjectsApp',
category: DEFAULT_APP_CATEGORIES.management,
navLinkId: PLUGIN_ID,
app: [PLUGIN_ID, 'kibana'],
catalogue: ['ingestManager'],

View file

@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server';
import { take } from 'rxjs/operators';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server';
// @ts-ignore
import { getEcommerceSavedObjects } from './sample_data/ecommerce_saved_objects';
@ -168,7 +169,8 @@ export class MapsPlugin implements Plugin {
name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', {
defaultMessage: 'Maps',
}),
order: 600,
order: 400,
category: DEFAULT_APP_CATEGORIES.kibana,
icon: APP_ICON,
navLinkId: APP_ID,
app: [APP_ID, 'kibana'],

View file

@ -15,6 +15,7 @@ import {
CapabilitiesStart,
IClusterClient,
} from 'kibana/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PluginsSetup, RouteInitialization } from './types';
import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app';
import { MlCapabilities } from '../common/types/capabilities';
@ -74,6 +75,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
}),
icon: PLUGIN_ICON,
order: 500,
category: DEFAULT_APP_CATEGORIES.kibana,
navLinkId: PLUGIN_ID,
app: [PLUGIN_ID, 'kibana'],
catalogue: [PLUGIN_ID, `${PLUGIN_ID}_file_data_visualizer`],

View file

@ -21,6 +21,7 @@ import {
CustomHttpResponseOptions,
ResponseError,
} from 'kibana/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import {
LOGGING_TAG,
KIBANA_MONITORING_LOGGING_TAG,
@ -248,6 +249,7 @@ export class Plugin {
name: i18n.translate('xpack.monitoring.featureRegistry.monitoringFeatureName', {
defaultMessage: 'Stack Monitoring',
}),
category: DEFAULT_APP_CATEGORIES.management,
icon: 'monitoringApp',
navLinkId: 'monitoring',
app: ['monitoring', 'kibana'],

View file

@ -21,6 +21,7 @@ export const createFeature = (
icon: 'discoverApp',
navLinkId: 'discover',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: [],
privileges:
privileges === null

View file

@ -32,6 +32,7 @@ const buildFeatures = () => {
name: 'Feature 1',
icon: 'addDataApp',
app: ['feature1App'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
app: ['feature1App'],
@ -56,6 +57,7 @@ const buildFeatures = () => {
name: 'Feature 2',
icon: 'addDataApp',
app: ['feature2App'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
app: ['feature2App'],

View file

@ -18,6 +18,7 @@ const buildProps = (customProps: any = {}) => {
id: 'feature1',
name: 'Feature 1',
app: ['app'],
category: { id: 'foo', label: 'foo' },
icon: 'spacesApp',
privileges: {
all: {

View file

@ -28,6 +28,7 @@ const features = [
id: 'normal',
name: 'normal feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: { all: [], read: [] },
@ -43,6 +44,7 @@ const features = [
id: 'normal_with_sub',
name: 'normal feature with sub features',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: { all: [], read: [] },
@ -96,6 +98,7 @@ const features = [
id: 'bothPrivilegesExcludedFromBase',
name: 'bothPrivilegesExcludedFromBase',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
excludeFromBasePrivileges: true,
@ -113,6 +116,7 @@ const features = [
id: 'allPrivilegeExcludedFromBase',
name: 'allPrivilegeExcludedFromBase',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
excludeFromBasePrivileges: true,

View file

@ -80,6 +80,7 @@ describe('usingPrivileges', () => {
id: 'fooFeature',
name: 'Foo KibanaFeature',
app: ['fooApp', 'foo'],
category: { id: 'foo', label: 'foo' },
navLinkId: 'foo',
privileges: null,
}),
@ -168,6 +169,7 @@ describe('usingPrivileges', () => {
id: 'fooFeature',
name: 'Foo KibanaFeature',
app: ['foo'],
category: { id: 'foo', label: 'foo' },
navLinkId: 'foo',
privileges: null,
}),
@ -322,6 +324,7 @@ describe('usingPrivileges', () => {
name: 'Foo KibanaFeature',
navLinkId: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
}),
new KibanaFeature({
@ -329,6 +332,7 @@ describe('usingPrivileges', () => {
name: 'Bar KibanaFeature',
navLinkId: 'bar',
app: ['bar'],
category: { id: 'foo', label: 'foo' },
privileges: null,
}),
],
@ -469,6 +473,7 @@ describe('usingPrivileges', () => {
name: 'Foo KibanaFeature',
navLinkId: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
}),
new KibanaFeature({
@ -476,6 +481,7 @@ describe('usingPrivileges', () => {
name: 'Bar KibanaFeature',
navLinkId: 'bar',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
}),
],
@ -552,6 +558,7 @@ describe('all', () => {
id: 'fooFeature',
name: 'Foo KibanaFeature',
app: ['foo'],
category: { id: 'foo', label: 'foo' },
navLinkId: 'foo',
privileges: null,
}),

View file

@ -33,6 +33,7 @@ describe(`feature_privilege_builder`, () => {
id: 'my-feature',
name: 'my-feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: privilege,
read: privilege,
@ -64,6 +65,7 @@ describe(`feature_privilege_builder`, () => {
id: 'my-feature',
name: 'my-feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: privilege,
read: privilege,
@ -101,6 +103,7 @@ describe(`feature_privilege_builder`, () => {
id: 'my-feature',
name: 'my-feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: privilege,
read: privilege,
@ -148,6 +151,7 @@ describe(`feature_privilege_builder`, () => {
id: 'my-feature',
name: 'my-feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: privilege,
read: privilege,

View file

@ -14,6 +14,7 @@ describe('featurePrivilegeIterator', () => {
name: 'foo',
privileges: null,
app: [],
category: { id: 'foo', label: 'foo' },
});
const actualPrivileges = Array.from(
@ -29,6 +30,7 @@ describe('featurePrivilegeIterator', () => {
const feature = new KibanaFeature({
id: 'foo',
name: 'foo',
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@ -120,6 +122,7 @@ describe('featurePrivilegeIterator', () => {
const feature = new KibanaFeature({
id: 'foo',
name: 'foo',
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@ -194,6 +197,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@ -317,6 +321,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@ -440,6 +445,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@ -567,6 +573,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@ -690,6 +697,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@ -815,6 +823,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -923,6 +932,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],

View file

@ -21,6 +21,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: ['app-1', 'app-2'],
category: { id: 'foo', label: 'foo' },
catalogue: ['catalogue-1', 'catalogue-2'],
management: {
foo: ['management-1', 'management-2'],
@ -66,6 +67,7 @@ describe('features', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -165,6 +167,7 @@ describe('features', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
}),
];
@ -207,6 +210,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@ -327,6 +331,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@ -409,6 +414,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@ -467,6 +473,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@ -532,6 +539,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@ -602,6 +610,7 @@ describe('reserved', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: ['app-1', 'app-2'],
category: { id: 'foo', label: 'foo' },
catalogue: ['catalogue-1', 'catalogue-2'],
management: {
foo: ['management-1', 'management-2'],
@ -644,6 +653,7 @@ describe('reserved', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
privileges: [
@ -708,6 +718,7 @@ describe('reserved', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -749,6 +760,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -876,6 +888,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -1075,6 +1088,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
excludeFromBasePrivileges: true,
privileges: {
all: {
@ -1216,6 +1230,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -1379,6 +1394,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
excludeFromBasePrivileges: true,
privileges: {
all: {
@ -1508,6 +1524,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {

View file

@ -12,6 +12,7 @@ it('allows features to be defined without privileges', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
});
@ -23,6 +24,7 @@ it('allows features with reserved privileges to be defined', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@ -49,6 +51,7 @@ it('allows features with sub-features to be defined', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -112,6 +115,7 @@ it('does not allow features with sub-features which have id conflicts with the m
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -162,6 +166,7 @@ it('does not allow features with sub-features which have id conflicts with the p
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@ -212,6 +217,7 @@ it('does not allow features with sub-features which have id conflicts each other
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {

View file

@ -13,6 +13,7 @@ it('allows features to be defined without privileges', () => {
name: 'foo',
app: [],
privileges: null,
category: { id: 'foo', label: 'foo' },
});
validateReservedPrivileges([feature]);
@ -23,6 +24,7 @@ it('allows features with a single reserved privilege to be defined', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@ -49,6 +51,7 @@ it('allows multiple features with reserved privileges to be defined', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@ -71,6 +74,7 @@ it('allows multiple features with reserved privileges to be defined', () => {
id: 'foo2',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@ -97,6 +101,7 @@ it('prevents a feature from specifying the same reserved privilege id', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@ -135,6 +140,7 @@ it('prevents features from sharing a reserved privilege id', () => {
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@ -157,6 +163,7 @@ it('prevents features from sharing a reserved privilege id', () => {
id: 'foo2',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',

View file

@ -87,6 +87,7 @@ const putRoleTest = (
id: 'feature_1',
name: 'feature 1',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
ui: [],

View file

@ -16,6 +16,7 @@ import {
Plugin as IPlugin,
PluginInitializerContext,
SavedObjectsClient,
DEFAULT_APP_CATEGORIES,
} from '../../../../src/core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { DataPluginSetup, DataPluginStart } from '../../../../src/plugins/data/server/plugin';
@ -178,6 +179,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
}),
order: 1100,
icon: 'logoSecurity',
category: DEFAULT_APP_CATEGORIES.security,
navLinkId: APP_ID,
app: [...securitySubPlugins, 'kibana'],
catalogue: ['securitySolution'],

View file

@ -22,7 +22,7 @@ export const SecureSpaceMessage = (props: SecureSpaceMessageProps) => {
return (
<Fragment>
<EuiHorizontalRule />
<EuiText className="eui-textCenter">
<EuiText>
<p>
<FormattedMessage
id="xpack.spaces.management.secureSpaceMessage.howToAssignRoleToSpaceDescription"

View file

@ -7,8 +7,6 @@
import {
EuiDescribedFormGroup,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPopover,
EuiPopoverProps,
@ -75,66 +73,27 @@ export class CustomizeSpace extends Component<Props, State> {
description={this.getPanelDescription()}
fullWidth
>
<EuiFlexGroup responsive={false}>
<EuiFlexItem>
<EuiFormRow
label={i18n.translate('xpack.spaces.management.manageSpacePage.nameFormRowLabel', {
defaultMessage: 'Name',
})}
{...validator.validateSpaceName(this.props.space)}
fullWidth
>
<EuiFieldText
name="name"
data-test-subj="addSpaceName"
placeholder={i18n.translate(
'xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder',
{
defaultMessage: 'Awesome space',
}
)}
value={name}
onChange={this.onNameChange}
fullWidth
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow
label={i18n.translate(
'xpack.spaces.management.manageSpacePage.avatarFormRowLabel',
{
defaultMessage: 'Avatar',
}
)}
>
<EuiPopover
id="customizeAvatarPopover"
button={
<button
title={i18n.translate(
'xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip',
{
defaultMessage: 'Click to customize this space avatar',
}
)}
onClick={this.togglePopover}
>
<SpaceAvatar space={this.props.space} size="l" />
</button>
}
closePopover={this.closePopover}
{...extraPopoverProps}
ownFocus={true}
isOpen={this.state.customizingAvatar}
>
<div style={{ maxWidth: 240 }}>
<CustomizeSpaceAvatar space={this.props.space} onChange={this.onAvatarChange} />
</div>
</EuiPopover>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFormRow
label={i18n.translate('xpack.spaces.management.manageSpacePage.nameFormRowLabel', {
defaultMessage: 'Name',
})}
{...validator.validateSpaceName(this.props.space)}
fullWidth
>
<EuiFieldText
name="name"
data-test-subj="addSpaceName"
placeholder={i18n.translate(
'xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder',
{
defaultMessage: 'Awesome space',
}
)}
value={name}
onChange={this.onNameChange}
fullWidth
/>
</EuiFormRow>
<EuiSpacer />
@ -175,6 +134,37 @@ export class CustomizeSpace extends Component<Props, State> {
rows={2}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.spaces.management.manageSpacePage.avatarFormRowLabel', {
defaultMessage: 'Avatar',
})}
>
<EuiPopover
id="customizeAvatarPopover"
button={
<button
title={i18n.translate(
'xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip',
{
defaultMessage: 'Click to customize this space avatar',
}
)}
onClick={this.togglePopover}
>
<SpaceAvatar space={this.props.space} size="l" />
</button>
}
closePopover={this.closePopover}
{...extraPopoverProps}
ownFocus={true}
isOpen={this.state.customizingAvatar}
>
<div style={{ maxWidth: 240 }}>
<CustomizeSpaceAvatar space={this.props.space} onChange={this.onAvatarChange} />
</div>
</EuiPopover>
</EuiFormRow>
</EuiDescribedFormGroup>
</SectionPanel>
);

View file

@ -2,14 +2,14 @@
exports[`EnabledFeatures renders as expected 1`] = `
<SectionPanel
collapsible={true}
collapsible={false}
data-test-subj="enabled-features-panel"
description="Customize visible features"
initiallyCollapsed={true}
initiallyCollapsed={false}
title={
<span>
<FormattedMessage
defaultMessage="Customize feature display"
defaultMessage="Features"
id="xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage"
values={Object {}}
/>
@ -41,7 +41,7 @@ exports[`EnabledFeatures renders as expected 1`] = `
>
<h3>
<FormattedMessage
defaultMessage="Control which features are visible in this space."
defaultMessage="Set feature visibility for this space"
id="xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage"
values={Object {}}
/>
@ -63,16 +63,16 @@ exports[`EnabledFeatures renders as expected 1`] = `
</p>
<p>
<FormattedMessage
defaultMessage="Want to secure access? Go to {rolesLink}."
defaultMessage="If you wish to secure access to features, please {manageSecurityRoles}."
id="xpack.spaces.management.enabledSpaceFeatures.goToRolesLink"
values={
Object {
"rolesLink": <EuiLink
"manageSecurityRoles": <EuiLink
data-test-subj="goToRoles"
href="management"
>
<FormattedMessage
defaultMessage="Roles"
defaultMessage="manage security roles"
id="xpack.spaces.management.enabledSpaceFeatures.rolesLinkText"
values={Object {}}
/>
@ -89,6 +89,12 @@ exports[`EnabledFeatures renders as expected 1`] = `
Array [
Object {
"app": Array [],
"category": Object {
"euiIconType": "logoKibana",
"id": "kibana",
"label": "Kibana",
"order": 1000,
},
"icon": "spacesApp",
"id": "feature-1",
"name": "Feature 1",
@ -96,6 +102,12 @@ exports[`EnabledFeatures renders as expected 1`] = `
},
Object {
"app": Array [],
"category": Object {
"euiIconType": "logoKibana",
"id": "kibana",
"label": "Kibana",
"order": 1000,
},
"icon": "spacesApp",
"id": "feature-2",
"name": "Feature 2",

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLink } from '@elastic/eui';
import React from 'react';
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { Space } from '../../../../common/model/space';
import { SectionPanel } from '../section_panel';
import { mountWithIntl, nextTick, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { EnabledFeatures } from './enabled_features';
import { KibanaFeatureConfig } from '../../../../../features/public';
import { DEFAULT_APP_CATEGORIES } from '../../../../../../../src/core/public';
import { findTestSubject } from 'test_utils/find_test_subject';
import { EuiCheckboxProps } from '@elastic/eui';
const features: KibanaFeatureConfig[] = [
{
@ -18,6 +18,7 @@ const features: KibanaFeatureConfig[] = [
name: 'Feature 1',
icon: 'spacesApp',
app: [],
category: DEFAULT_APP_CATEGORIES.kibana,
privileges: null,
},
{
@ -25,16 +26,11 @@ const features: KibanaFeatureConfig[] = [
name: 'Feature 2',
icon: 'spacesApp',
app: [],
category: DEFAULT_APP_CATEGORIES.kibana,
privileges: null,
},
];
const space: Space = {
id: 'my-space',
name: 'my space',
disabledFeatures: ['feature-1', 'feature-2'],
};
describe('EnabledFeatures', () => {
const getUrlForApp = (appId: string) => appId;
@ -43,7 +39,11 @@ describe('EnabledFeatures', () => {
shallowWithIntl<EnabledFeatures>(
<EnabledFeatures
features={features}
space={space}
space={{
id: 'my-space',
name: 'my space',
disabledFeatures: ['feature-1', 'feature-2'],
}}
securityEnabled={true}
onChange={jest.fn()}
getUrlForApp={getUrlForApp}
@ -52,27 +52,33 @@ describe('EnabledFeatures', () => {
).toMatchSnapshot();
});
it('allows all features to be toggled on', () => {
it('allows all features in a category to be toggled on', () => {
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
<EnabledFeatures
features={features}
space={space}
space={{
id: 'my-space',
name: 'my space',
disabledFeatures: ['feature-1', 'feature-2'],
}}
securityEnabled={true}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
// expand section panel
wrapper.find(SectionPanel).find(EuiLink).simulate('click');
// Click the "Change all" link
wrapper.find('.spcToggleAllFeatures__changeAllLink').first().simulate('click');
// Click category-level toggle
const {
onChange = () => {
throw new Error('expected onChange to be defined');
},
} = wrapper.find('input#featureCategoryCheckbox_kibana').props() as EuiCheckboxProps;
onChange({ target: { checked: true } } as any);
// Ask to show all features
wrapper.find('button[data-test-subj="spc-toggle-all-features-show"]').simulate('click');
findTestSubject(wrapper, `featureCategoryButton_kibana`).simulate('click');
expect(changeHandler).toBeCalledTimes(1);
@ -81,27 +87,36 @@ describe('EnabledFeatures', () => {
expect(updatedSpace.disabledFeatures).toEqual([]);
});
it('allows all features to be toggled off', () => {
it('allows all features in a category to be toggled off', async () => {
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
<EnabledFeatures
features={features}
space={space}
space={{
id: 'my-space',
name: 'my space',
disabledFeatures: [],
}}
securityEnabled={true}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
// expand section panel
wrapper.find(SectionPanel).find(EuiLink).simulate('click');
// Click category-level toggle
const {
onChange = () => {
throw new Error('expected onChange to be defined');
},
} = wrapper.find('input#featureCategoryCheckbox_kibana').props() as EuiCheckboxProps;
onChange({ target: { checked: false } } as any);
// Click the "Change all" link
wrapper.find('.spcToggleAllFeatures__changeAllLink').first().simulate('click');
// Ask to show all features
findTestSubject(wrapper, `featureCategoryButton_kibana`).simulate('click');
// Ask to hide all features
wrapper.find('button[data-test-subj="spc-toggle-all-features-hide"]').simulate('click');
await nextTick();
wrapper.update();
expect(changeHandler).toBeCalledTimes(1);
@ -109,4 +124,140 @@ describe('EnabledFeatures', () => {
expect(updatedSpace.disabledFeatures).toEqual(['feature-1', 'feature-2']);
});
it('allows all features to be toggled off', async () => {
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
<EnabledFeatures
features={features}
space={{
id: 'my-space',
name: 'my space',
disabledFeatures: [],
}}
securityEnabled={true}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
// show should not be visible when all features are already visible
expect(findTestSubject(wrapper, 'showAllFeaturesLink')).toHaveLength(0);
findTestSubject(wrapper, 'hideAllFeaturesLink').simulate('click');
await nextTick();
wrapper.update();
expect(changeHandler).toBeCalledTimes(1);
const updatedSpace = changeHandler.mock.calls[0][0];
expect(updatedSpace.disabledFeatures).toEqual(['feature-1', 'feature-2']);
});
it('allows all features to be toggled on', async () => {
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
<EnabledFeatures
features={features}
space={{
id: 'my-space',
name: 'my space',
disabledFeatures: ['feature-1', 'feature-2'],
}}
securityEnabled={true}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
// hide should not be visible when all features are already hidden
expect(findTestSubject(wrapper, 'hideAllFeaturesLink')).toHaveLength(0);
findTestSubject(wrapper, 'showAllFeaturesLink').simulate('click');
await nextTick();
wrapper.update();
expect(changeHandler).toBeCalledTimes(1);
const updatedSpace = changeHandler.mock.calls[0][0];
expect(updatedSpace.disabledFeatures).toEqual([]);
});
it('displays both show and hide options when a non-zero subset of features are toggled on', async () => {
const wrapper = mountWithIntl(
<EnabledFeatures
features={features}
space={{
id: 'my-space',
name: 'my space',
disabledFeatures: ['feature-1'],
}}
securityEnabled={true}
onChange={jest.fn()}
getUrlForApp={getUrlForApp}
/>
);
expect(findTestSubject(wrapper, 'hideAllFeaturesLink')).toHaveLength(1);
expect(findTestSubject(wrapper, 'showAllFeaturesLink')).toHaveLength(1);
});
describe('feature category button', () => {
it(`does not toggle visibility when it contains more than one item`, () => {
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
<EnabledFeatures
features={features}
space={{
id: 'my-space',
name: 'my space',
disabledFeatures: [],
}}
securityEnabled={true}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
findTestSubject(wrapper, `featureCategoryButton_kibana`).simulate('click');
expect(changeHandler).not.toHaveBeenCalled();
});
it('toggles item visibility when the category contains a single item', () => {
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
<EnabledFeatures
features={[
...features,
{
id: 'feature-3',
name: 'Feature 3',
icon: 'spacesApp',
app: [],
category: DEFAULT_APP_CATEGORIES.management,
privileges: null,
},
]}
space={{
id: 'my-space',
name: 'my space',
disabledFeatures: [],
}}
securityEnabled={true}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
findTestSubject(wrapper, `featureCategoryButton_management`).simulate('click');
expect(changeHandler).toBeCalledTimes(1);
const updatedSpace = changeHandler.mock.calls[0][0];
expect(updatedSpace.disabledFeatures).toEqual(['feature-3']);
});
});
});

View file

@ -34,8 +34,8 @@ export class EnabledFeatures extends Component<Props, {}> {
return (
<SectionPanel
collapsible
initiallyCollapsed
collapsible={false}
initiallyCollapsed={false}
title={this.getPanelTitle()}
description={description}
data-test-subj="enabled-features-panel"
@ -46,7 +46,7 @@ export class EnabledFeatures extends Component<Props, {}> {
<h3>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage"
defaultMessage="Control which features are visible in this space."
defaultMessage="Set feature visibility for this space"
/>
</h3>
</EuiTitle>
@ -114,7 +114,7 @@ export class EnabledFeatures extends Component<Props, {}> {
<span>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage"
defaultMessage="Customize feature display"
defaultMessage="Features"
/>{' '}
{details}
</span>
@ -135,16 +135,16 @@ export class EnabledFeatures extends Component<Props, {}> {
<p>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.goToRolesLink"
defaultMessage="Want to secure access? Go to {rolesLink}."
defaultMessage="If you wish to secure access to features, please {manageSecurityRoles}."
values={{
rolesLink: (
manageSecurityRoles: (
<EuiLink
data-test-subj="goToRoles"
href={this.props.getUrlForApp('management', { path: 'security/roles' })}
>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.rolesLinkText"
defaultMessage="Roles"
defaultMessage="manage security roles"
/>
</EuiLink>
),

View file

@ -0,0 +1,4 @@
.spcFeatureTableAccordionContent {
// Align accordion content with the feature category logo in the accordion's buttonContent
padding-left: $euiSizeXL;
}

View file

@ -4,14 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiIcon, EuiInMemoryTable, EuiSwitch, EuiText, IconType } from '@elastic/eui';
import { EuiCallOut } from '@elastic/eui';
import {
EuiAccordion,
EuiCheckbox,
EuiCheckboxProps,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AppCategory } from 'kibana/public';
import _ from 'lodash';
import React, { ChangeEvent, Component } from 'react';
import React, { ChangeEvent, Component, ReactElement } from 'react';
import { KibanaFeatureConfig } from '../../../../../../plugins/features/public';
import { Space } from '../../../../common/model/space';
import { ToggleAllFeatures } from './toggle_all_features';
import { getEnabledFeatures } from '../../lib/feature_utils';
import './feature_table.scss';
interface Props {
space: Partial<Space>;
@ -20,15 +35,201 @@ interface Props {
}
export class FeatureTable extends Component<Props, {}> {
private featureCategories: Map<string, KibanaFeatureConfig[]> = new Map();
constructor(props: Props) {
super(props);
// features are static for the lifetime of the page, so this is safe to do here in a non-reactive manner
props.features.forEach((feature) => {
if (!this.featureCategories.has(feature.category.id)) {
this.featureCategories.set(feature.category.id, []);
}
this.featureCategories.get(feature.category.id)!.push(feature);
});
}
public render() {
const { space, features } = this.props;
const { space } = this.props;
const items = features.map((feature) => ({
feature,
space,
}));
const accordions: Array<{ order: number; element: ReactElement }> = [];
this.featureCategories.forEach((featuresInCategory) => {
const { category } = featuresInCategory[0];
return <EuiInMemoryTable columns={this.getColumns()} items={items} />;
const featureCount = featuresInCategory.length;
const enabledCount = getEnabledFeatures(featuresInCategory, space).length;
const canExpandCategory = featuresInCategory.length > 1;
const checkboxProps: EuiCheckboxProps = {
id: `featureCategoryCheckbox_${category.id}`,
indeterminate: enabledCount > 0 && enabledCount < featureCount,
checked: featureCount === enabledCount,
['aria-label']: i18n.translate(
'xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel',
{ defaultMessage: 'Category toggle' }
),
onClick: (e) => {
// Clicking the checkbox should not cause the accordion to expand.
// Stopping event propagation ensures this.
e.stopPropagation();
},
onChange: (e) => {
this.setFeaturesVisibility(
featuresInCategory.map((f) => f.id),
e.target.checked
);
},
};
const buttonContent = (
<EuiFlexGroup
data-test-subj={`featureCategoryButton_${category.id}`}
alignItems={'center'}
responsive={false}
gutterSize="m"
onClick={() => {
if (!canExpandCategory) {
const isChecked = enabledCount > 0;
this.setFeaturesVisibility(
featuresInCategory.map((f) => f.id),
!isChecked
);
}
}}
>
<EuiFlexItem grow={false}>
<EuiCheckbox {...checkboxProps} />
</EuiFlexItem>
{category.euiIconType ? (
<EuiFlexItem grow={false}>
<EuiIcon size="m" type={category.euiIconType} />
</EuiFlexItem>
) : null}
<EuiFlexItem grow={1}>
<EuiTitle size="xs">
<h4 className="eui-displayInlineBlock">{category.label}</h4>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
);
const label: string = i18n.translate('xpack.spaces.management.featureAccordionSwitchLabel', {
defaultMessage: '{enabledCount} / {featureCount} features visible',
values: {
enabledCount,
featureCount,
},
});
const extraAction = (
<EuiText size="s" aria-hidden="true" color={'subdued'}>
{label}
</EuiText>
);
const helpText = this.getCategoryHelpText(category);
const accordion = (
<EuiAccordion
id={`featureCategory_${category.id}`}
data-test-subj={`featureCategory_${category.id}`}
key={category.id}
arrowDisplay={canExpandCategory ? 'right' : 'none'}
forceState={canExpandCategory ? undefined : 'closed'}
buttonContent={buttonContent}
extraAction={canExpandCategory ? extraAction : undefined}
>
<div className="spcFeatureTableAccordionContent">
<EuiSpacer size="s" />
{helpText && (
<>
<EuiCallOut iconType="iInCircle" size="s">
{helpText}
</EuiCallOut>
<EuiSpacer size="s" />
</>
)}
{featuresInCategory.map((feature) => {
const featureChecked = !(
space.disabledFeatures && space.disabledFeatures.includes(feature.id)
);
return (
<EuiFlexGroup key={`${feature.id}-toggle`}>
<EuiFlexItem grow={false}>
<EuiCheckbox
id={`featureCheckbox_${feature.id}`}
data-test-subj={`featureCheckbox_${feature.id}`}
checked={featureChecked}
onChange={this.onChange(feature.id) as any}
label={feature.name}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
})}
</div>
</EuiAccordion>
);
accordions.push({
order: category.order ?? Number.MAX_SAFE_INTEGER,
element: accordion,
});
});
accordions.sort((a1, a2) => a1.order - a2.order);
const featureCount = this.props.features.length;
const enabledCount = getEnabledFeatures(this.props.features, this.props.space).length;
const controls = [];
if (enabledCount < featureCount) {
controls.push(
<EuiLink onClick={() => this.showAll()} data-test-subj="showAllFeaturesLink">
<EuiText size="xs">
{i18n.translate('xpack.spaces.management.selectAllFeaturesLink', {
defaultMessage: 'Select all',
})}
</EuiText>
</EuiLink>
);
}
if (enabledCount > 0) {
controls.push(
<EuiLink onClick={() => this.hideAll()} data-test-subj="hideAllFeaturesLink">
<EuiText size="xs">
{i18n.translate('xpack.spaces.management.deselectAllFeaturesLink', {
defaultMessage: 'Deselect all',
})}
</EuiText>
</EuiLink>
);
}
return (
<div>
<EuiFlexGroup alignItems={'flexEnd'}>
<EuiFlexItem>
<EuiText size="xs">
<b>
{i18n.translate('xpack.spaces.management.featureVisibilityTitle', {
defaultMessage: 'Feature visibility',
})}
</b>
</EuiText>
</EuiFlexItem>
{controls.map((control, idx) => (
<EuiFlexItem grow={false} key={idx}>
{control}
</EuiFlexItem>
))}
</EuiFlexGroup>
<EuiHorizontalRule margin={'m'} />
{accordions.flatMap((a, idx) => [
a.element,
<EuiHorizontalRule key={`accordion-hr-${idx}`} margin={'m'} />,
])}
</div>
);
}
public onChange = (featureId: string) => (e: ChangeEvent<HTMLInputElement>) => {
@ -49,67 +250,41 @@ export class FeatureTable extends Component<Props, {}> {
this.props.onChange(updatedSpace);
};
private onChangeAll = (visible: boolean) => {
private getAllFeatureIds = () =>
[...this.featureCategories.values()].flat().map((feature) => feature.id);
private hideAll = () => {
this.setFeaturesVisibility(this.getAllFeatureIds(), false);
};
private showAll = () => {
this.setFeaturesVisibility(this.getAllFeatureIds(), true);
};
private setFeaturesVisibility = (features: string[], visible: boolean) => {
const updatedSpace: Partial<Space> = {
...this.props.space,
};
if (visible) {
updatedSpace.disabledFeatures = [];
updatedSpace.disabledFeatures = (updatedSpace.disabledFeatures ?? []).filter(
(df) => !features.includes(df)
);
} else {
updatedSpace.disabledFeatures = this.props.features.map((feature) => feature.id);
updatedSpace.disabledFeatures = Array.from(
new Set([...(updatedSpace.disabledFeatures ?? []), ...features])
);
}
this.props.onChange(updatedSpace);
};
private getColumns = () => [
{
field: 'feature',
name: i18n.translate('xpack.spaces.management.enabledSpaceFeaturesFeatureColumnTitle', {
defaultMessage: 'Feature',
}),
render: (
feature: KibanaFeatureConfig,
_item: { feature: KibanaFeatureConfig; space: Props['space'] }
) => {
return (
<EuiText>
<EuiIcon size="m" type={feature.icon as IconType} />
&ensp; {feature.name}
</EuiText>
);
},
},
{
field: 'space',
width: '150',
name: (
<span>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeaturesEnabledColumnTitle"
defaultMessage="Show?"
/>
<ToggleAllFeatures onChange={this.onChangeAll} />
</span>
),
render: (spaceEntry: Space, record: Record<string, any>) => {
const checked = !(
spaceEntry.disabledFeatures && spaceEntry.disabledFeatures.includes(record.feature.id)
);
return (
<EuiSwitch
data-test-subj={`feature-${record.feature.id}-toggle`}
id={record.feature.id}
checked={checked}
onChange={this.onChange(record.feature.id) as any}
label={`${record.feature.name} visible`}
showLabel={false}
/>
);
},
},
];
private getCategoryHelpText = (category: AppCategory) => {
if (category.id === 'management') {
return i18n.translate('xpack.spaces.management.managementCategoryHelpText', {
defaultMessage:
'Access to Stack Management is determined by your privileges, and cannot be hidden by Spaces.',
});
}
};
}

View file

@ -4,19 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiLink, EuiSwitch } from '@elastic/eui';
import { EuiButton, EuiCheckboxProps } from '@elastic/eui';
import { ReactWrapper } from 'enzyme';
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal';
import { ManageSpacePage } from './manage_space_page';
import { SectionPanel } from './section_panel';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { SpacesManager } from '../../spaces_manager';
import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mocks';
import { featuresPluginMock } from '../../../../features/public/mocks';
import { KibanaFeature } from '../../../../features/public';
import { DEFAULT_APP_CATEGORIES } from '../../../../../../src/core/public';
// To be resolved by EUI team.
// https://github.com/elastic/eui/issues/3712
@ -39,6 +39,7 @@ featuresStart.getFeatures.mockResolvedValue([
name: 'feature 1',
icon: 'spacesApp',
app: [],
category: DEFAULT_APP_CATEGORIES.kibana,
privileges: null,
}),
]);
@ -309,16 +310,12 @@ function updateSpace(wrapper: ReactWrapper<any, any>, updateFeature = true) {
}
function toggleFeature(wrapper: ReactWrapper<any, any>) {
const featureSectionButton = wrapper
.find(SectionPanel)
.filter('[data-test-subj="enabled-features-panel"]')
.find(EuiLink);
featureSectionButton.simulate('click');
wrapper.update();
wrapper.find(EuiSwitch).find('button').simulate('click');
const {
onChange = () => {
throw new Error('expected onChange to be defined');
},
} = wrapper.find('input#featureCategoryCheckbox_kibana').props() as EuiCheckboxProps;
onChange({ target: { checked: false } } as any);
wrapper.update();
}

View file

@ -177,11 +177,16 @@ export class ManageSpacePage extends Component<Props, State> {
};
public getFormHeading = () => (
<EuiTitle size="m">
<h1>
{this.getTitle()} <ReservedSpaceBadge space={this.state.space as Space} />
</h1>
</EuiTitle>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle size="m">
<h1 className="eui-displayInlineBlock">{this.getTitle()}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ReservedSpaceBadge space={this.state.space as Space} />
</EuiFlexItem>
</EuiFlexGroup>
);
public getTitle = () => {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiIcon } from '@elastic/eui';
import { EuiBadge } from '@elastic/eui';
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { ReservedSpaceBadge } from './reserved_space_badge';
@ -24,7 +24,7 @@ const unreservedSpace = {
test('it renders without crashing', () => {
const wrapper = shallowWithIntl(<ReservedSpaceBadge space={reservedSpace} />);
expect(wrapper.find(EuiIcon)).toHaveLength(1);
expect(wrapper.find(EuiBadge)).toHaveLength(1);
});
test('it renders nothing for an unreserved space', () => {

View file

@ -6,7 +6,7 @@
import React from 'react';
import { EuiIcon, EuiToolTip } from '@elastic/eui';
import { EuiBadge, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { isReservedSpace } from '../../../common';
import { Space } from '../../../common/model/space';
@ -28,7 +28,9 @@ export const ReservedSpaceBadge = (props: Props) => {
/>
}
>
<EuiIcon style={{ verticalAlign: 'super' }} type={'lock'} />
<EuiBadge color="warning" iconType="questionInCircle" iconSide="right">
Reserved space
</EuiBadge>
</EuiToolTip>
);
}

View file

@ -47,6 +47,7 @@ featuresStart.getFeatures.mockResolvedValue([
name: 'feature 1',
icon: 'spacesApp',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
}),
]);

View file

@ -88,7 +88,11 @@ describe('spacesManagementApp', () => {
expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Spaces' }]);
expect(container).toMatchInlineSnapshot(`
<div>
Spaces Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"securityEnabled":true}
<div
class="kbnRedirectCrossAppLinks"
>
Spaces Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"securityEnabled":true}
</div>
</div>
`);
@ -107,7 +111,11 @@ describe('spacesManagementApp', () => {
]);
expect(container).toMatchInlineSnapshot(`
<div>
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"securityEnabled":true}
<div
class="kbnRedirectCrossAppLinks"
>
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"securityEnabled":true}
</div>
</div>
`);
@ -128,7 +136,11 @@ describe('spacesManagementApp', () => {
]);
expect(container).toMatchInlineSnapshot(`
<div>
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"securityEnabled":true}
<div
class="kbnRedirectCrossAppLinks"
>
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"securityEnabled":true}
</div>
</div>
`);

View file

@ -9,6 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { Router, Route, Switch, useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { StartServicesAccessor } from 'src/core/public';
import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
import { SecurityLicense } from '../../../security/public';
import { RegisterManagementAppArgs } from '../../../../../src/plugins/management/public';
import { PluginsStart } from '../plugin';
@ -32,6 +33,7 @@ export const spacesManagementApp = Object.freeze({
title: i18n.translate('xpack.spaces.displayName', {
defaultMessage: 'Spaces',
}),
async mount({ element, setBreadcrumbs, history }) {
const [
{ notifications, i18n: i18nStart, application },
@ -114,19 +116,21 @@ export const spacesManagementApp = Object.freeze({
render(
<i18nStart.Context>
<Router history={history}>
<Switch>
<Route path={['', '/']} exact>
<SpacesGridPageWithBreadcrumbs />
</Route>
<Route path="/create">
<CreateSpacePageWithBreadcrumbs />
</Route>
<Route path="/edit/:spaceId">
<EditSpacePageWithBreadcrumbs />
</Route>
</Switch>
</Router>
<RedirectAppLinks application={application}>
<Router history={history}>
<Switch>
<Route path={['', '/']} exact>
<SpacesGridPageWithBreadcrumbs />
</Route>
<Route path="/create">
<CreateSpacePageWithBreadcrumbs />
</Route>
<Route path="/edit/:spaceId">
<EditSpacePageWithBreadcrumbs />
</Route>
</Switch>
</Router>
</RedirectAppLinks>
</i18nStart.Context>,
element
);

View file

@ -17549,13 +17549,10 @@
"xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(表示されているすべての機能)",
"xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "機能の表示をカスタマイズ",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "このスペースでどの機能が表示されるかを管理します。",
"xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "セキュアなアクセスをご希望の場合は、{rolesLink} にアクセスしてください。",
"xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(表示されている機能がありません)",
"xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "この機能は UI で非表示になっていますが、無効ではありません。",
"xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "ロール",
"xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "({featureCount} 件中 {enabledCount} 件の機能を表示中)",
"xpack.spaces.management.enabledSpaceFeaturesEnabledColumnTitle": "表示しますか?",
"xpack.spaces.management.enabledSpaceFeaturesFeatureColumnTitle": "機能",
"xpack.spaces.management.hideAllFeaturesText": "すべて非表示",
"xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "アバター",
"xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "素晴らしいスペース",
@ -17563,10 +17560,8 @@
"xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "クリックしてこのスペースのアバターをカスタマイズします",
"xpack.spaces.management.manageSpacePage.createSpaceButton": "スペースを作成",
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "スペースの作成",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "スペースに名前を付けてアバターをカスタマイズします",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "URL 識別子に注意してください。スペースの作成後に変更することはできません。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 識別子は変更できません。",
"xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "スペースのカスタマイズ",
"xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "表示される機能のカスタマイズ",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "スペースの読み込み中にエラーが発生: {message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "スペースの保存中にエラーが発生: {message}",

View file

@ -17559,13 +17559,10 @@
"xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(所有可见功能)",
"xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "定制功能显示",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "控制哪些功能在此工作区中可见。",
"xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "想保护访问?前往 {rolesLink}。",
"xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(没有可见功能)",
"xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "该功能在 UI 中已隐藏,但未禁用。",
"xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "角色",
"xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "{enabledCount} / {featureCount} 个功能可见)",
"xpack.spaces.management.enabledSpaceFeaturesEnabledColumnTitle": "显示?",
"xpack.spaces.management.enabledSpaceFeaturesFeatureColumnTitle": "功能",
"xpack.spaces.management.hideAllFeaturesText": "全部隐藏",
"xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "头像",
"xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "超卓的空间",
@ -17573,10 +17570,8 @@
"xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "单击可定制此工作区头像",
"xpack.spaces.management.manageSpacePage.createSpaceButton": "创建工作区",
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建一个空间",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "命名您的工作区并定制其头像。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "记下 URL 标识符。创建工作区后,将不能更改它。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 标识符无法更改。",
"xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "定制您的工作区",
"xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "定制可见功能",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "加载空间时出错:{message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "保存空间时出错:{message}",

View file

@ -5,6 +5,7 @@
*/
import { Request, Server } from 'hapi';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PLUGIN } from '../common/constants/plugin';
import { compose } from './lib/compose/kibana';
import { initUptimeServer } from './uptime_server';
@ -31,6 +32,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
id: PLUGIN.ID,
name: PLUGIN.NAME,
order: 1000,
category: DEFAULT_APP_CATEGORIES.observability,
navLinkId: PLUGIN.ID,
icon: 'uptimeApp',
app: ['uptime', 'kibana'],

View file

@ -51,7 +51,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('a11y test for for customize space card', async () => {
await PageObjects.spaceSelector.clickEnterSpaceName();
await PageObjects.spaceSelector.addSpaceName('space_a');
await PageObjects.spaceSelector.clickSpaceAcustomAvatar();
await PageObjects.spaceSelector.clickCustomizeSpaceAvatar('space_a');
await a11y.testAppSnapshot();
await browser.pressKeys(browser.keys.ESCAPE);
});
@ -75,30 +75,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
it('a11y test for click on "show" button to open customize feature display', async () => {
await retry.waitFor(
'show button is visible',
async () => await testSubjects.exists('show-hide-section-link')
);
await PageObjects.spaceSelector.clickShowFeatures();
it('a11y test for toggling an entire feature category', async () => {
await PageObjects.spaceSelector.toggleFeatureCategoryVisibility('kibana');
await a11y.testAppSnapshot();
});
it('a11y test for change all option for feature visibility popover', async () => {
await PageObjects.spaceSelector.clickFeaturesVisibilityButton();
await PageObjects.spaceSelector.openFeatureCategory('kibana');
await a11y.testAppSnapshot();
});
it('a11y test for hide all feature visibility popover option', async () => {
await PageObjects.spaceSelector.clickHideAllFeatures();
await a11y.testAppSnapshot();
});
it('a11y test for toggle individual feature - using enterprise feature visibility', async () => {
await PageObjects.spaceSelector.clickFeaturesVisibilityButton();
await PageObjects.spaceSelector.clickShowAllFeatures();
await PageObjects.spaceSelector.toggleFeatureVisibility('enterpriseSearch');
await a11y.testAppSnapshot();
await PageObjects.spaceSelector.toggleFeatureCategoryVisibility('kibana');
});
it('a11y test for space listing page', async () => {

View file

@ -76,6 +76,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
id: 'actionsSimulators',
name: 'actionsSimulators',
app: ['actions', 'kibana'],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
app: ['actions', 'kibana'],

View file

@ -36,6 +36,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
id: 'alertsFixture',
name: 'Alerts',
app: ['alerts', 'kibana'],
category: { id: 'foo', label: 'foo' },
alerting: [
'test.always-firing',
'test.cumulative-firing',

View file

@ -27,6 +27,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
id: 'alertsRestrictedFixture',
name: 'AlertRestricted',
app: ['alerts', 'kibana'],
category: { id: 'foo', label: 'foo' },
alerting: ['test.restricted-noop', 'test.unrestricted-noop', 'test.noop'],
privileges: {
all: {

View file

@ -66,8 +66,8 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro
await testSubjects.setValue('addSpaceName', spaceName);
}
async clickSpaceAcustomAvatar() {
await testSubjects.click('space-avatar-space_a');
async clickCustomizeSpaceAvatar(spaceId: string) {
await testSubjects.click(`space-avatar-${spaceId}`);
}
async clickSpaceInitials() {
@ -122,10 +122,6 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro
await testSubjects.setValue('spaceURLDisplay', spaceURL);
}
async clickFeaturesVisibilityButton() {
await testSubjects.click('changeAllFeatureVisibilityPopover');
}
async clickHideAllFeatures() {
await testSubjects.click('spc-toggle-all-features-hide');
}
@ -134,8 +130,28 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro
await testSubjects.click('spc-toggle-all-features-show');
}
async toggleFeatureVisibility(featureName: string) {
await testSubjects.click(`feature-${featureName}-toggle`);
async openFeatureCategory(categoryName: string) {
const category = await find.byCssSelector(
`button[aria-controls=featureCategory_${categoryName}]`
);
const isCategoryExpanded = (await category.getAttribute('aria-expanded')) === 'true';
if (!isCategoryExpanded) {
await category.click();
}
}
async closeFeatureCategory(categoryName: string) {
const category = await find.byCssSelector(
`button[aria-controls=featureCategory_${categoryName}]`
);
const isCategoryExpanded = (await category.getAttribute('aria-expanded')) === 'true';
if (isCategoryExpanded) {
await category.click();
}
}
async toggleFeatureCategoryVisibility(categoryName: string) {
await testSubjects.click(`featureCategoryButton_${categoryName}`);
}
async clickOnDescriptionOfSpace() {

View file

@ -25,6 +25,7 @@ export class AlertingFixturePlugin implements Plugin<void, void, AlertingExample
id: 'alerting_fixture',
name: 'alerting_fixture',
app: [],
category: { id: 'foo', label: 'foo' },
alerting: ['test.always-firing', 'test.noop'],
privileges: {
all: {

View file

@ -18,6 +18,7 @@ class FooPlugin implements Plugin {
id: 'foo',
name: 'Foo',
icon: 'upArrow',
category: { id: 'foo', label: 'foo' },
navLinkId: 'foo_plugin',
app: ['foo_plugin', 'kibana'],
catalogue: ['foo'],