mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[index patterns] index pattern create modal (#101853)
index pattern creation flyout
This commit is contained in:
parent
923eca0adf
commit
d44df74598
185 changed files with 3400 additions and 5834 deletions
|
@ -36,6 +36,7 @@
|
|||
"monaco": "packages/kbn-monaco/src",
|
||||
"esQuery": "packages/kbn-es-query/src",
|
||||
"presentationUtil": "src/plugins/presentation_util",
|
||||
"indexPatternEditor": "src/plugins/index_pattern_editor",
|
||||
"indexPatternFieldEditor": "src/plugins/index_pattern_field_editor",
|
||||
"indexPatternManagement": "src/plugins/index_pattern_management",
|
||||
"interactiveSetup": "src/plugins/interactive_setup",
|
||||
|
|
|
@ -123,6 +123,10 @@ for use in their own application.
|
|||
|Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/index_pattern_editor/README.md[indexPatternEditor]
|
||||
|Create index patterns from within Kibana apps.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/index_pattern_field_editor/README.md[indexPatternFieldEditor]
|
||||
|The reusable field editor across Kibana!
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [hideCloseButton](./kibana-plugin-core-public.overlayflyoutopenoptions.hideclosebutton.md)
|
||||
|
||||
## OverlayFlyoutOpenOptions.hideCloseButton property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hideCloseButton?: boolean;
|
||||
```
|
|
@ -18,6 +18,7 @@ export interface OverlayFlyoutOpenOptions
|
|||
| ["data-test-subj"](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) | <code>string</code> | |
|
||||
| [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | <code>string</code> | |
|
||||
| [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | <code>string</code> | |
|
||||
| [hideCloseButton](./kibana-plugin-core-public.overlayflyoutopenoptions.hideclosebutton.md) | <code>boolean</code> | |
|
||||
| [maxWidth](./kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md) | <code>boolean | number | string</code> | |
|
||||
| [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | <code>boolean</code> | |
|
||||
| [size](./kibana-plugin-core-public.overlayflyoutopenoptions.size.md) | <code>EuiFlyoutSize</code> | |
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAggRestrictions](./kibana-plugin-plugins-data-public.indexpatternaggrestrictions.md)
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggregationRestrictions](./kibana-plugin-plugins-data-public.aggregationrestrictions.md)
|
||||
|
||||
## IndexPatternAggRestrictions type
|
||||
## AggregationRestrictions type
|
||||
|
||||
<b>Signature:</b>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) > [allowNoIndex](./kibana-plugin-plugins-data-public.getfieldsoptions.allownoindex.md)
|
||||
|
||||
## GetFieldsOptions.allowNoIndex property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
allowNoIndex?: boolean;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) > [lookBack](./kibana-plugin-plugins-data-public.getfieldsoptions.lookback.md)
|
||||
|
||||
## GetFieldsOptions.lookBack property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
lookBack?: boolean;
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md)
|
||||
|
||||
## GetFieldsOptions interface
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface GetFieldsOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [allowNoIndex](./kibana-plugin-plugins-data-public.getfieldsoptions.allownoindex.md) | <code>boolean</code> | |
|
||||
| [lookBack](./kibana-plugin-plugins-data-public.getfieldsoptions.lookback.md) | <code>boolean</code> | |
|
||||
| [metaFields](./kibana-plugin-plugins-data-public.getfieldsoptions.metafields.md) | <code>string[]</code> | |
|
||||
| [pattern](./kibana-plugin-plugins-data-public.getfieldsoptions.pattern.md) | <code>string</code> | |
|
||||
| [rollupIndex](./kibana-plugin-plugins-data-public.getfieldsoptions.rollupindex.md) | <code>string</code> | |
|
||||
| [type](./kibana-plugin-plugins-data-public.getfieldsoptions.type.md) | <code>string</code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) > [metaFields](./kibana-plugin-plugins-data-public.getfieldsoptions.metafields.md)
|
||||
|
||||
## GetFieldsOptions.metaFields property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
metaFields?: string[];
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) > [pattern](./kibana-plugin-plugins-data-public.getfieldsoptions.pattern.md)
|
||||
|
||||
## GetFieldsOptions.pattern property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
pattern: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) > [rollupIndex](./kibana-plugin-plugins-data-public.getfieldsoptions.rollupindex.md)
|
||||
|
||||
## GetFieldsOptions.rollupIndex property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
rollupIndex?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) > [type](./kibana-plugin-plugins-data-public.getfieldsoptions.type.md)
|
||||
|
||||
## GetFieldsOptions.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type?: string;
|
||||
```
|
|
@ -63,6 +63,7 @@
|
|||
| [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | Data plugin public Start contract |
|
||||
| [DataPublicPluginStartActions](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.md) | utilities to generate filters from action context |
|
||||
| [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) | Data plugin prewired UI components |
|
||||
| [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) | |
|
||||
| [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) | |
|
||||
| [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | |
|
||||
| [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) | |
|
||||
|
@ -141,6 +142,7 @@
|
|||
| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | |
|
||||
| [AggGroupName](./kibana-plugin-plugins-data-public.agggroupname.md) | |
|
||||
| [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | |
|
||||
| [AggregationRestrictions](./kibana-plugin-plugins-data-public.aggregationrestrictions.md) | |
|
||||
| [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | AggsStart represents the actual external contract as AggsCommonStart is only used internally. The difference is that AggsStart includes the typings for the registry with initialized agg types. |
|
||||
| [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | \* |
|
||||
| [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md) | |
|
||||
|
@ -163,7 +165,6 @@
|
|||
| [IFieldParamType](./kibana-plugin-plugins-data-public.ifieldparamtype.md) | |
|
||||
| [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) | |
|
||||
| [IMetricAggType](./kibana-plugin-plugins-data-public.imetricaggtype.md) | |
|
||||
| [IndexPatternAggRestrictions](./kibana-plugin-plugins-data-public.indexpatternaggrestrictions.md) | |
|
||||
| [IndexPatternLoadExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.indexpatternloadexpressionfunctiondefinition.md) | |
|
||||
| [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) | |
|
||||
| [IndexPatternSelectProps](./kibana-plugin-plugins-data-public.indexpatternselectprops.md) | |
|
||||
|
|
|
@ -34,6 +34,7 @@ pageLoadAssetSize:
|
|||
indexLifecycleManagement: 107090
|
||||
indexManagement: 140608
|
||||
indexPatternManagement: 28222
|
||||
indexPatternEditor: 25000
|
||||
infra: 184320
|
||||
fleet: 465774
|
||||
ingestPipelines: 58003
|
||||
|
@ -119,4 +120,3 @@ pageLoadAssetSize:
|
|||
expressionMetric: 22238
|
||||
expressionShape: 34008
|
||||
interactiveSetup: 18532
|
||||
|
|
@ -84,6 +84,7 @@ export interface OverlayFlyoutOpenOptions {
|
|||
'data-test-subj'?: string;
|
||||
size?: EuiFlyoutSize;
|
||||
maxWidth?: boolean | number | string;
|
||||
hideCloseButton?: boolean;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
|
|
|
@ -1027,6 +1027,8 @@ export interface OverlayFlyoutOpenOptions {
|
|||
// (undocumented)
|
||||
closeButtonAriaLabel?: string;
|
||||
// (undocumented)
|
||||
hideCloseButton?: boolean;
|
||||
// (undocumented)
|
||||
maxWidth?: boolean | number | string;
|
||||
// (undocumented)
|
||||
ownFocus?: boolean;
|
||||
|
|
|
@ -528,7 +528,7 @@ export class IndexPatternsService {
|
|||
const indexPattern = await this.create(spec, skipFetchFields);
|
||||
const createdIndexPattern = await this.createSavedObject(indexPattern, override);
|
||||
await this.setDefault(createdIndexPattern.id!);
|
||||
return createdIndexPattern;
|
||||
return createdIndexPattern!;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -83,7 +83,9 @@ export {
|
|||
IndexPatternSpec,
|
||||
IndexPatternLoadExpressionFunctionDefinition,
|
||||
fieldList,
|
||||
GetFieldsOptions,
|
||||
INDEX_PATTERN_SAVED_OBJECT_TYPE,
|
||||
AggregationRestrictions,
|
||||
IndexPatternType,
|
||||
} from '../common';
|
||||
|
||||
|
|
|
@ -538,6 +538,22 @@ export class AggParamType<TAggConfig extends IAggConfig = IAggConfig> extends Ba
|
|||
makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AggregationRestrictions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
type AggregationRestrictions = Record<string, {
|
||||
agg?: string;
|
||||
interval?: number;
|
||||
fixed_interval?: string;
|
||||
calendar_interval?: string;
|
||||
delay?: string;
|
||||
time_zone?: string;
|
||||
}>;
|
||||
|
||||
export { AggregationRestrictions }
|
||||
|
||||
export { AggregationRestrictions as IndexPatternAggRestrictions }
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "AggsCommonStart" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public
|
||||
|
@ -987,6 +1003,24 @@ export function getEsPreference(uiSettings: IUiSettingsClient_2, sessionId?: str
|
|||
// @public (undocumented)
|
||||
export function getEsQueryConfig(config: KibanaConfig): EsQueryConfig_2;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "GetFieldsOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface GetFieldsOptions {
|
||||
// (undocumented)
|
||||
allowNoIndex?: boolean;
|
||||
// (undocumented)
|
||||
lookBack?: boolean;
|
||||
// (undocumented)
|
||||
metaFields?: string[];
|
||||
// (undocumented)
|
||||
pattern: string;
|
||||
// (undocumented)
|
||||
rollupIndex?: string;
|
||||
// (undocumented)
|
||||
type?: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getKbnTypeNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public @deprecated (undocumented)
|
||||
|
@ -1303,18 +1337,6 @@ export class IndexPattern implements IIndexPattern {
|
|||
version: string | undefined;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AggregationRestrictions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type IndexPatternAggRestrictions = Record<string, {
|
||||
agg?: string;
|
||||
interval?: number;
|
||||
fixed_interval?: string;
|
||||
calendar_interval?: string;
|
||||
delay?: string;
|
||||
time_zone?: string;
|
||||
}>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "IndexPatternAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
|
@ -1506,7 +1528,6 @@ export class IndexPatternsService {
|
|||
getDefault: () => Promise<IndexPattern | null>;
|
||||
getDefaultId: () => Promise<string | null>;
|
||||
getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any>;
|
||||
// Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts
|
||||
getFieldsForWildcard: (options: GetFieldsOptions) => Promise<any>;
|
||||
getIds: (refresh?: boolean) => Promise<string[]>;
|
||||
getIdsWithTitle: (refresh?: boolean) => Promise<Array<{
|
||||
|
@ -1535,7 +1556,7 @@ export enum IndexPatternType {
|
|||
// @public (undocumented)
|
||||
export interface IndexPatternTypeMeta {
|
||||
// (undocumented)
|
||||
aggs?: Record<string, IndexPatternAggRestrictions>;
|
||||
aggs?: Record<string, AggregationRestrictions>;
|
||||
// (undocumented)
|
||||
params?: {
|
||||
rollup_index: string;
|
||||
|
@ -2483,20 +2504,20 @@ export interface WaitUntilNextSessionCompletesOptions {
|
|||
// src/plugins/data/public/index.ts:54:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:54:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:54:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:225:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:225:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:225:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:227:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:228:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:237:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:238:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:239:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:240:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:244:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:245:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:248:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:249:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:252:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:227:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:227:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:227:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:229:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:230:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:239:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:240:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:241:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:242:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:246:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:247:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:250:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:251:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:254:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/search/session/session_service.ts:62:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
|
48
src/plugins/index_pattern_editor/README.md
Normal file
48
src/plugins/index_pattern_editor/README.md
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Index pattern editor
|
||||
|
||||
Create index patterns from within Kibana apps.
|
||||
|
||||
## How to use
|
||||
|
||||
You first need to add in your kibana.json the "`indexPatternEditor`" plugin as a required dependency of your plugin.
|
||||
|
||||
You will then receive in the start contract of the indexPatternEditor plugin the following API:
|
||||
|
||||
### `userPermissions.editIndexPattern(): boolean`
|
||||
|
||||
Convenience method that uses the `core.application.capabilities` api to determine whether the user can create or edit the index pattern.
|
||||
|
||||
### `openEditor(options: IndexPatternEditorProps): CloseEditor`
|
||||
|
||||
Use this method to display the index pattern editor to create an index pattern.
|
||||
|
||||
#### `options`
|
||||
|
||||
`onSave: (indexPattern: IndexPattern) => void` (**required**)
|
||||
|
||||
You must provide an `onSave` handler to be notified when an index pattern has been created/updated. This handler is called after the index pattern has been persisted as a saved object.
|
||||
|
||||
`onCancel: () => void;` (optional)
|
||||
|
||||
You can optionally pass an `onCancel` handler which is called when the index pattern creation flyout is closed wihtout creating an index pattern.
|
||||
|
||||
`defaultTypeIsRollup: boolean` (optional, default false)
|
||||
|
||||
The default index pattern type can be optionally specified as `rollup`.
|
||||
|
||||
`requireTimestampField: boolean` (optional, default false)
|
||||
|
||||
The editor can require a timestamp field on the index pattern.
|
||||
|
||||
### IndexPatternEditorComponent
|
||||
|
||||
This the React component interface equivalent to `openEditor`. It takes the same arguments -
|
||||
|
||||
```tsx
|
||||
<IndexPatternEditorComponent
|
||||
onSave={...}
|
||||
onCancel={...}
|
||||
defaultTypeIsRollup={false}
|
||||
requireTimestampField={false}
|
||||
/>
|
||||
```
|
13
src/plugins/index_pattern_editor/jest.config.js
Normal file
13
src/plugins/index_pattern_editor/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/src/plugins/index_pattern_editor'],
|
||||
};
|
13
src/plugins/index_pattern_editor/kibana.json
Normal file
13
src/plugins/index_pattern_editor/kibana.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "indexPatternEditor",
|
||||
"version": "kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["data"],
|
||||
"requiredBundles": ["kibanaReact", "esUiShared"],
|
||||
"owner": {
|
||||
"name": "App Services",
|
||||
"githubTeam": "kibana-app-services"
|
||||
},
|
||||
"description": "This plugin provides the ability to create index patterns via a modal flyout from any kibana app"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
%inp-empty-state-footer {
|
||||
background: $euiColorLightestShade;
|
||||
margin: 0 (-$euiSizeL) (-$euiSizeL);
|
||||
padding: $euiSizeL;
|
||||
border-radius: 0 0 $euiBorderRadius $euiBorderRadius;
|
||||
|
||||
// sass-lint:disable-block mixins-before-declarations
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
$inpEmptyStateMaxWidth: $euiSizeXXL * 19;
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { UseField, TextField, ToggleField } from '../../shared_imports';
|
||||
import { IndexPatternConfig } from '../../types';
|
||||
|
||||
import { AdvancedParamsSection } from './advanced_params_section';
|
||||
|
||||
const allowHiddenAriaLabel = i18n.translate('indexPatternEditor.form.allowHiddenAriaLabel', {
|
||||
defaultMessage: 'Allow hidden and system indices',
|
||||
});
|
||||
|
||||
const customIndexPatternIdLabel = i18n.translate(
|
||||
'indexPatternEditor.form.customIndexPatternIdLabel',
|
||||
{
|
||||
defaultMessage: 'Custom index pattern ID',
|
||||
}
|
||||
);
|
||||
|
||||
interface AdvancedParamsContentProps {
|
||||
disableAllowHidden: boolean;
|
||||
}
|
||||
|
||||
export const AdvancedParamsContent = ({ disableAllowHidden }: AdvancedParamsContentProps) => (
|
||||
<AdvancedParamsSection>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<UseField<boolean, IndexPatternConfig>
|
||||
path={'allowHidden'}
|
||||
component={ToggleField}
|
||||
data-test-subj="allowHiddenField"
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'aria-label': allowHiddenAriaLabel,
|
||||
disabled: disableAllowHidden,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<UseField<string, IndexPatternConfig>
|
||||
path={'id'}
|
||||
component={TextField}
|
||||
data-test-subj="savedObjectIdField"
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'aria-label': customIndexPatternIdLabel,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</AdvancedParamsSection>
|
||||
);
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AdvancedParamsSection = ({ children }: Props) => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
|
||||
const toggleIsVisible = useCallback(() => {
|
||||
setIsVisible(!isVisible);
|
||||
}, [isVisible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty onClick={toggleIsVisible} flush="left" data-test-subj="toggleAdvancedSetting">
|
||||
{isVisible
|
||||
? i18n.translate('indexPatternEditor.editor.form.advancedSettings.hideButtonLabel', {
|
||||
defaultMessage: 'Hide advanced settings',
|
||||
})
|
||||
: i18n.translate('indexPatternEditor.editor.form.advancedSettings.showButtonLabel', {
|
||||
defaultMessage: 'Show advanced settings',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<div style={{ display: isVisible ? 'block' : 'none' }} data-test-subj="advancedSettings">
|
||||
<EuiSpacer size="m" />
|
||||
{/* We ned to wrap the children inside a "div" to have our css :first-child rule */}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { AdvancedParamsContent } from './advanced_params_content';
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmptyState should render normally 1`] = `
|
||||
exports[`EmptyIndexListPrompt should render normally 1`] = `
|
||||
<Fragment>
|
||||
<EuiPageContent
|
||||
className="inpEmptyState"
|
||||
|
@ -16,7 +16,7 @@ exports[`EmptyState should render normally 1`] = `
|
|||
<h2>
|
||||
<FormattedMessage
|
||||
defaultMessage="Ready to try Kibana? First, you need data."
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.noDataTitle"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.noDataTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h2>
|
||||
|
@ -38,7 +38,7 @@ exports[`EmptyState should render normally 1`] = `
|
|||
description={
|
||||
<FormattedMessage
|
||||
defaultMessage="Add data from a variety of sources."
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.integrationCardDescription"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.integrationCardDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ exports[`EmptyState should render normally 1`] = `
|
|||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Add integration"
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.integrationCardTitle"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.integrationCardTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ exports[`EmptyState should render normally 1`] = `
|
|||
description={
|
||||
<FormattedMessage
|
||||
defaultMessage="Import a CSV, NDJSON, or log file."
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.uploadCardDescription"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.uploadCardDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ exports[`EmptyState should render normally 1`] = `
|
|||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload a file"
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.uploadCardTitle"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.uploadCardTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ exports[`EmptyState should render normally 1`] = `
|
|||
description={
|
||||
<FormattedMessage
|
||||
defaultMessage="Load a data set and a Kibana dashboard."
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.sampleDataCardDescription"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.sampleDataCardDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ exports[`EmptyState should render normally 1`] = `
|
|||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Add sample data"
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.sampleDataCardTitle"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.sampleDataCardTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -131,18 +131,18 @@ exports[`EmptyState should render normally 1`] = `
|
|||
Object {
|
||||
"description": <EuiLink
|
||||
external={true}
|
||||
href="https://www.elastic.co/guide/en/kibana/mocked-test-branch/connect-to-elasticsearch.html"
|
||||
href="http://elastic.co"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Read documentation"
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.readDocs"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.readDocs"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>,
|
||||
"title": <FormattedMessage
|
||||
defaultMessage="Want to learn more?"
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.learnMore"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.learnMore"
|
||||
values={Object {}}
|
||||
/>,
|
||||
},
|
||||
|
@ -164,7 +164,7 @@ exports[`EmptyState should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Check for new data"
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.checkDataButton"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.checkDataButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
||||
|
@ -175,7 +175,7 @@ exports[`EmptyState should render normally 1`] = `
|
|||
</EuiLink>,
|
||||
"title": <FormattedMessage
|
||||
defaultMessage="Think you already have data?"
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.haveData"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.haveData"
|
||||
values={Object {}}
|
||||
/>,
|
||||
},
|
||||
|
@ -184,33 +184,33 @@ exports[`EmptyState should render normally 1`] = `
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
textAlign="center"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Some indices may be hidden. Try to {link} anyway."
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.createAnyway"
|
||||
values={
|
||||
Object {
|
||||
"link": <EuiLink
|
||||
data-test-subj="createAnyway"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="create an index pattern"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.createAnywayLink"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiText>
|
||||
</div>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
<EuiSpacer />
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
textAlign="center"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Some indices may be hidden. Try to {link} anyway."
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.createAnyway"
|
||||
values={
|
||||
Object {
|
||||
"link": <EuiLink
|
||||
data-test-subj="createAnyway"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="create an index pattern"
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.createAnywayLink"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,5 +1,5 @@
|
|||
@import '../../../variables';
|
||||
@import '../../../templates';
|
||||
@import '../../variables';
|
||||
@import '../../templates';
|
||||
|
||||
.inpEmptyState {
|
||||
// override EUI specificity
|
|
@ -7,14 +7,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EmptyState } from '../empty_state';
|
||||
import { EmptyIndexListPrompt } from './empty_index_list_prompt';
|
||||
import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { docLinksServiceMock } from '../../../../../../core/public/mocks';
|
||||
|
||||
const docLinks = docLinksServiceMock.createStartContract();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({
|
||||
|
@ -22,14 +19,16 @@ jest.mock('react-router-dom', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
describe('EmptyState', () => {
|
||||
describe('EmptyIndexListPrompt', () => {
|
||||
it('should render normally', () => {
|
||||
const component = shallow(
|
||||
<EmptyState
|
||||
docLinks={docLinks}
|
||||
<EmptyIndexListPrompt
|
||||
onRefresh={() => {}}
|
||||
navigateToApp={async () => {}}
|
||||
canSave={true}
|
||||
createAnyway={() => {}}
|
||||
closeFlyout={() => {}}
|
||||
addDataUrl={'http://elastic.co'}
|
||||
navigateToApp={async (appId) => {}}
|
||||
canSaveIndexPattern={true}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -42,11 +41,13 @@ describe('EmptyState', () => {
|
|||
const onRefreshHandler = sinon.stub();
|
||||
|
||||
const component = mountWithIntl(
|
||||
<EmptyState
|
||||
docLinks={docLinks}
|
||||
<EmptyIndexListPrompt
|
||||
onRefresh={onRefreshHandler}
|
||||
navigateToApp={async () => {}}
|
||||
canSave={true}
|
||||
createAnyway={() => {}}
|
||||
closeFlyout={() => {}}
|
||||
addDataUrl={'http://elastic.co'}
|
||||
navigateToApp={async (appId) => {}}
|
||||
canSaveIndexPattern={true}
|
||||
/>
|
||||
);
|
||||
|
|
@ -6,10 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import './empty_state.scss';
|
||||
import './empty_index_list_prompt.scss';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { DocLinksStart, ApplicationStart } from 'kibana/public';
|
||||
import {
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
|
@ -26,30 +25,34 @@ import {
|
|||
EuiText,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { reactRouterNavigate } from '../../../../../../plugins/kibana_react/public';
|
||||
|
||||
export const EmptyState = ({
|
||||
import { ApplicationStart } from 'src/core/public';
|
||||
|
||||
export const EmptyIndexListPrompt = ({
|
||||
onRefresh,
|
||||
closeFlyout,
|
||||
createAnyway,
|
||||
canSaveIndexPattern,
|
||||
addDataUrl,
|
||||
navigateToApp,
|
||||
docLinks,
|
||||
canSave,
|
||||
}: {
|
||||
onRefresh: () => void;
|
||||
closeFlyout: () => void;
|
||||
createAnyway: () => void;
|
||||
canSaveIndexPattern: boolean;
|
||||
addDataUrl: string;
|
||||
navigateToApp: ApplicationStart['navigateToApp'];
|
||||
docLinks: DocLinksStart;
|
||||
canSave: boolean;
|
||||
}) => {
|
||||
const createAnyway = (
|
||||
const createAnywayLink = (
|
||||
<EuiText color="subdued" textAlign="center" size="xs">
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.createAnyway"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.createAnyway"
|
||||
defaultMessage="Some indices may be hidden. Try to {link} anyway."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink {...reactRouterNavigate(useHistory(), 'create')} data-test-subj="createAnyway">
|
||||
<EuiLink onClick={() => createAnyway()} data-test-subj="createAnyway">
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.createAnywayLink"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.createAnywayLink"
|
||||
defaultMessage="create an index pattern"
|
||||
/>
|
||||
</EuiLink>
|
||||
|
@ -74,7 +77,7 @@ export const EmptyState = ({
|
|||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.noDataTitle"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.noDataTitle"
|
||||
defaultMessage="Ready to try Kibana? First, you need data."
|
||||
/>
|
||||
</h2>
|
||||
|
@ -87,17 +90,20 @@ export const EmptyState = ({
|
|||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
className="inpEmptyState__card"
|
||||
onClick={() => navigateToApp('home', { path: '#/tutorial_directory' })}
|
||||
onClick={() => {
|
||||
navigateToApp('home', { path: '#/tutorial_directory' });
|
||||
closeFlyout();
|
||||
}}
|
||||
icon={<EuiIcon size="xl" type="database" color="subdued" />}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.integrationCardTitle"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.integrationCardTitle"
|
||||
defaultMessage="Add integration"
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.integrationCardDescription"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.integrationCardDescription"
|
||||
defaultMessage="Add data from a variety of sources."
|
||||
/>
|
||||
}
|
||||
|
@ -110,13 +116,13 @@ export const EmptyState = ({
|
|||
icon={<EuiIcon size="xl" type="document" color="subdued" />}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.uploadCardTitle"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.uploadCardTitle"
|
||||
defaultMessage="Upload a file"
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.uploadCardDescription"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.uploadCardDescription"
|
||||
defaultMessage="Import a CSV, NDJSON, or log file."
|
||||
/>
|
||||
}
|
||||
|
@ -125,17 +131,20 @@ export const EmptyState = ({
|
|||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
className="inpEmptyState__card"
|
||||
onClick={() => navigateToApp('home', { path: '#/tutorial_directory/sampleData' })}
|
||||
onClick={() => {
|
||||
navigateToApp('home', { path: '#/tutorial_directory/sampleData' });
|
||||
closeFlyout();
|
||||
}}
|
||||
icon={<EuiIcon size="xl" type="heatmap" color="subdued" />}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.sampleDataCardTitle"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.sampleDataCardTitle"
|
||||
defaultMessage="Add sample data"
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.sampleDataCardDescription"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.sampleDataCardDescription"
|
||||
defaultMessage="Load a data set and a Kibana dashboard."
|
||||
/>
|
||||
}
|
||||
|
@ -151,14 +160,14 @@ export const EmptyState = ({
|
|||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.learnMore"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.learnMore"
|
||||
defaultMessage="Want to learn more?"
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<EuiLink href={docLinks.links.addData} target="_blank" external>
|
||||
<EuiLink href={addDataUrl} target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.readDocs"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.readDocs"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
|
@ -173,14 +182,14 @@ export const EmptyState = ({
|
|||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.haveData"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.haveData"
|
||||
defaultMessage="Think you already have data?"
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<EuiLink onClick={onRefresh} data-test-subj="refreshIndicesButton">
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.emptyState.checkDataButton"
|
||||
id="indexPatternEditor.createIndexPattern.emptyState.checkDataButton"
|
||||
defaultMessage="Check for new data"
|
||||
/>{' '}
|
||||
<EuiIcon type="refresh" size="s" />
|
||||
|
@ -191,11 +200,11 @@ export const EmptyState = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{canSaveIndexPattern && createAnywayLink}
|
||||
</div>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
<EuiSpacer />
|
||||
{canSave && createAnyway}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { EmptyIndexListPrompt } from './empty_index_list_prompt';
|
|
@ -19,7 +19,15 @@ exports[`EmptyIndexPatternPrompt should render normally 1`] = `
|
|||
className="inpEmptyIndexPatternPrompt__illustration"
|
||||
grow={1}
|
||||
>
|
||||
<IndexPatternIllustration />
|
||||
<Suspense
|
||||
fallback={
|
||||
<EuiLoadingSpinner
|
||||
size="xl"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<lazy />
|
||||
</Suspense>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="inpEmptyIndexPatternPrompt__text"
|
||||
|
@ -31,39 +39,35 @@ exports[`EmptyIndexPatternPrompt should render normally 1`] = `
|
|||
<h2>
|
||||
<FormattedMessage
|
||||
defaultMessage="You have data in Elasticsearch."
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.youHaveData"
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.youHaveData"
|
||||
values={Object {}}
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
defaultMessage="Now, create an index pattern."
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.nowCreate"
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.nowCreate"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Kibana requires an index pattern to identify which indices you want to explore. An index pattern can point to a specific index, for example, your log data from yesterday, or all indices that contain your log data."
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation"
|
||||
defaultMessage="Kibana requires an index pattern to identify which data streams, indices, and aliases you want to explore. An index pattern can point to a specific index, for example, your log data from yesterday, or all indices that contain your log data."
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.indexPatternExplanation"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<CreateButton
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"onClick": [Function],
|
||||
"text": "default",
|
||||
},
|
||||
]
|
||||
}
|
||||
<EuiButton
|
||||
data-test-subj="createIndexPatternButtonFlyout"
|
||||
fill={true}
|
||||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Create index pattern"
|
||||
id="indexPatternManagement.indexPatternTable.createBtn"
|
||||
id="indexPatternEditor.indexPatternTable.createBtn"
|
||||
values={Object {}}
|
||||
/>
|
||||
</CreateButton>
|
||||
</EuiButton>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -79,19 +83,19 @@ exports[`EmptyIndexPatternPrompt should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Want to learn more?"
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.learnMore"
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.learnMore"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiLink
|
||||
external={true}
|
||||
href="testUrl"
|
||||
href="http://elastic.co/"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Read documentation"
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.documentation"
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.documentation"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>
|
|
@ -537,4 +537,5 @@ const IndexPatternIllustration = () => (
|
|||
</svg>
|
||||
);
|
||||
|
||||
export const Illustration = IndexPatternIllustration;
|
||||
/* eslint-disable import/no-default-export */
|
||||
export default IndexPatternIllustration;
|
|
@ -1,5 +1,5 @@
|
|||
@import '../../../variables';
|
||||
@import '../../../templates';
|
||||
@import '../../variables';
|
||||
@import '../../templates';
|
||||
|
||||
.inpEmptyIndexPatternPrompt {
|
||||
// override EUI specificity
|
|
@ -14,10 +14,9 @@ describe('EmptyIndexPatternPrompt', () => {
|
|||
it('should render normally', () => {
|
||||
const component = shallowWithI18nProvider(
|
||||
<EmptyIndexPatternPrompt
|
||||
canSave
|
||||
creationOptions={[{ text: 'default', onClick: () => {} }]}
|
||||
docLinksIndexPatternIntro={'testUrl'}
|
||||
setBreadcrumbs={() => {}}
|
||||
goToCreate={() => {}}
|
||||
canSaveIndexPattern={true}
|
||||
indexPatternsIntroUrl={'http://elastic.co/'}
|
||||
/>
|
||||
);
|
||||
|
|
@ -8,34 +8,26 @@
|
|||
|
||||
import './empty_index_pattern_prompt.scss';
|
||||
|
||||
import React from 'react';
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { EuiPageContent, EuiSpacer, EuiText, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import { EuiDescriptionListTitle } from '@elastic/eui';
|
||||
import { EuiDescriptionListDescription, EuiDescriptionList } from '@elastic/eui';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { getListBreadcrumbs } from '../../breadcrumbs';
|
||||
import { IndexPatternCreationOption } from '../../types';
|
||||
import { CreateButton } from '../../create_button';
|
||||
import { Illustration } from './assets/index_pattern_illustration';
|
||||
import { ManagementAppMountParams } from '../../../../../management/public';
|
||||
|
||||
import { EuiLink, EuiButton, EuiLoadingSpinner } from '@elastic/eui';
|
||||
interface Props {
|
||||
canSave: boolean;
|
||||
creationOptions: IndexPatternCreationOption[];
|
||||
docLinksIndexPatternIntro: string;
|
||||
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
|
||||
goToCreate: () => void;
|
||||
canSaveIndexPattern: boolean;
|
||||
indexPatternsIntroUrl: string;
|
||||
}
|
||||
|
||||
export const EmptyIndexPatternPrompt = ({
|
||||
canSave,
|
||||
creationOptions,
|
||||
docLinksIndexPatternIntro,
|
||||
setBreadcrumbs,
|
||||
}: Props) => {
|
||||
setBreadcrumbs(getListBreadcrumbs());
|
||||
const Illustration = lazy(() => import('./assets/index_pattern_illustration'));
|
||||
|
||||
export const EmptyIndexPatternPrompt = ({
|
||||
goToCreate,
|
||||
canSaveIndexPattern,
|
||||
indexPatternsIntroUrl,
|
||||
}: Props) => {
|
||||
return (
|
||||
<EuiPageContent
|
||||
data-test-subj="emptyIndexPatternPrompt"
|
||||
|
@ -47,36 +39,43 @@ export const EmptyIndexPatternPrompt = ({
|
|||
>
|
||||
<EuiFlexGroup gutterSize="xl" alignItems="center" direction="rowReverse" wrap>
|
||||
<EuiFlexItem grow={1} className="inpEmptyIndexPatternPrompt__illustration">
|
||||
<Illustration />
|
||||
<Suspense fallback={<EuiLoadingSpinner size="xl" />}>
|
||||
<Illustration />
|
||||
</Suspense>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2} className="inpEmptyIndexPatternPrompt__text">
|
||||
<EuiText grow={false}>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.youHaveData"
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.youHaveData"
|
||||
defaultMessage="You have data in Elasticsearch."
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.nowCreate"
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.nowCreate"
|
||||
defaultMessage="Now, create an index pattern."
|
||||
/>
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation"
|
||||
defaultMessage="Kibana requires an index pattern to identify which indices you want to explore. An
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.indexPatternExplanation"
|
||||
defaultMessage="Kibana requires an index pattern to identify which data streams, indices, and aliases you want to explore. An
|
||||
index pattern can point to a specific index, for example, your log data from
|
||||
yesterday, or all indices that contain your log data."
|
||||
/>
|
||||
</p>
|
||||
{canSave && (
|
||||
<CreateButton options={creationOptions}>
|
||||
{canSaveIndexPattern && (
|
||||
<EuiButton
|
||||
onClick={goToCreate}
|
||||
iconType="plusInCircle"
|
||||
fill={true}
|
||||
data-test-subj="createIndexPatternButtonFlyout"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.indexPatternTable.createBtn"
|
||||
id="indexPatternEditor.indexPatternTable.createBtn"
|
||||
defaultMessage="Create index pattern"
|
||||
/>
|
||||
</CreateButton>
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
@ -85,14 +84,14 @@ export const EmptyIndexPatternPrompt = ({
|
|||
<EuiDescriptionList className="inpEmptyIndexPatternPrompt__footer" type="responsiveColumn">
|
||||
<EuiDescriptionListTitle className="inpEmptyIndexPatternPrompt__title">
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.learnMore"
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.learnMore"
|
||||
defaultMessage="Want to learn more?"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiLink href={docLinksIndexPatternIntro} target="_blank" external>
|
||||
<EuiLink href={indexPatternsIntroUrl} target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.emptyIndexPatternPrompt.documentation"
|
||||
id="indexPatternEditor.emptyIndexPatternPrompt.documentation"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiLink>
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, FC } from 'react';
|
||||
|
||||
import { useKibana } from '../../shared_imports';
|
||||
|
||||
import { MatchedItem, ResolveIndexResponseItemAlias, IndexPatternEditorContext } from '../../types';
|
||||
|
||||
import { getIndices } from '../../lib';
|
||||
|
||||
import { EmptyIndexListPrompt } from './empty_index_list_prompt';
|
||||
import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt';
|
||||
import { PromptFooter } from './prompt_footer';
|
||||
|
||||
const removeAliases = (item: MatchedItem) =>
|
||||
!((item as unknown) as ResolveIndexResponseItemAlias).indices;
|
||||
|
||||
interface Props {
|
||||
onCancel: () => void;
|
||||
allSources: MatchedItem[];
|
||||
hasExistingIndexPatterns: boolean;
|
||||
loadSources: () => void;
|
||||
}
|
||||
|
||||
export const EmptyPrompts: FC<Props> = ({
|
||||
hasExistingIndexPatterns,
|
||||
allSources,
|
||||
onCancel,
|
||||
children,
|
||||
loadSources,
|
||||
}) => {
|
||||
const {
|
||||
services: { docLinks, application, http },
|
||||
} = useKibana<IndexPatternEditorContext>();
|
||||
|
||||
const [remoteClustersExist, setRemoteClustersExist] = useState<boolean>(false);
|
||||
const [goToForm, setGoToForm] = useState<boolean>(false);
|
||||
|
||||
const hasDataIndices = allSources.some(({ name }: MatchedItem) => !name.startsWith('.'));
|
||||
|
||||
useCallback(() => {
|
||||
let isMounted = true;
|
||||
if (!hasDataIndices)
|
||||
getIndices(http, () => false, '*:*', false).then((dataSources) => {
|
||||
if (isMounted) {
|
||||
setRemoteClustersExist(!!dataSources.filter(removeAliases).length);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [http, hasDataIndices]);
|
||||
|
||||
if (!hasExistingIndexPatterns && !goToForm) {
|
||||
if (!hasDataIndices && !remoteClustersExist) {
|
||||
// load data
|
||||
return (
|
||||
<>
|
||||
<EmptyIndexListPrompt
|
||||
onRefresh={loadSources}
|
||||
closeFlyout={onCancel}
|
||||
createAnyway={() => setGoToForm(true)}
|
||||
canSaveIndexPattern={application.capabilities.indexPatterns.save as boolean}
|
||||
navigateToApp={application.navigateToApp}
|
||||
addDataUrl={docLinks.links.indexPatterns.introduction}
|
||||
/>
|
||||
<PromptFooter onCancel={onCancel} />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
// first time
|
||||
return (
|
||||
<>
|
||||
<EmptyIndexPatternPrompt
|
||||
goToCreate={() => setGoToForm(true)}
|
||||
indexPatternsIntroUrl={docLinks.links.indexPatterns.introduction}
|
||||
canSaveIndexPattern={application.capabilities.indexPatterns.save as boolean}
|
||||
/>
|
||||
<PromptFooter onCancel={onCancel} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { LoadingState } from './loading_state';
|
||||
export { EmptyPrompts } from './empty_prompts';
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { Header } from './header';
|
||||
export { PromptFooter } from './prompt_footer';
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
|
||||
|
||||
const closeButtonLabel = i18n.translate(
|
||||
'indexPatternEditor.editor.emptyPrompt.flyoutCloseButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Close',
|
||||
}
|
||||
);
|
||||
|
||||
interface PromptFooterProps {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const PromptFooter = ({ onCancel }: PromptFooterProps) => {
|
||||
return (
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={onCancel}
|
||||
data-test-subj="closeFlyoutButton"
|
||||
>
|
||||
{closeButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
CSSProperties,
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
useCallback,
|
||||
createContext,
|
||||
useContext,
|
||||
} from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { useFlyoutPanelsContext } from './flyout_panels';
|
||||
|
||||
interface Context {
|
||||
registerFooter: () => void;
|
||||
registerContent: () => void;
|
||||
}
|
||||
|
||||
const flyoutPanelContext = createContext<Context>({
|
||||
registerFooter: () => {},
|
||||
registerContent: () => {},
|
||||
});
|
||||
|
||||
export interface Props {
|
||||
/** Width of the panel (in percent %) */
|
||||
width?: number;
|
||||
/** EUI sass background */
|
||||
backgroundColor?: 'euiPageBackground' | 'euiEmptyShade';
|
||||
/** Add a border to the panel */
|
||||
border?: 'left' | 'right';
|
||||
}
|
||||
|
||||
export const Panel: React.FC<Props & React.HTMLProps<HTMLDivElement>> = ({
|
||||
children,
|
||||
width,
|
||||
className = '',
|
||||
backgroundColor,
|
||||
border,
|
||||
...rest
|
||||
}) => {
|
||||
const [config, setConfig] = useState<{ hasFooter: boolean; hasContent: boolean }>({
|
||||
hasContent: false,
|
||||
hasFooter: false,
|
||||
});
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const classes = classnames('fieldEditor__flyoutPanel', className, {
|
||||
'fieldEditor__flyoutPanel--pageBackground': backgroundColor === 'euiPageBackground',
|
||||
'fieldEditor__flyoutPanel--emptyShade': backgroundColor === 'euiEmptyShade',
|
||||
'fieldEditor__flyoutPanel--leftBorder': border === 'left',
|
||||
'fieldEditor__flyoutPanel--rightBorder': border === 'right',
|
||||
'fieldEditor__flyoutPanel--withContent': config.hasContent,
|
||||
});
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
const { addPanel } = useFlyoutPanelsContext();
|
||||
|
||||
const registerContent = useCallback(() => {
|
||||
setConfig((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
hasContent: true,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const registerFooter = useCallback(() => {
|
||||
setConfig((prev) => {
|
||||
if (!prev.hasContent) {
|
||||
throw new Error(
|
||||
'You need to add a <FlyoutPanels.Content /> when you add a <FlyoutPanels.Footer />'
|
||||
);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
hasFooter: true,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const removePanel = addPanel({ width });
|
||||
|
||||
return removePanel;
|
||||
}, [width, addPanel]);
|
||||
|
||||
const styles: CSSProperties = {};
|
||||
|
||||
if (width) {
|
||||
styles.flexBasis = `${width}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem style={styles}>
|
||||
<flyoutPanelContext.Provider value={{ registerContent, registerFooter }}>
|
||||
<div className={classes} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
</flyoutPanelContext.Provider>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFlyoutPanelContext = (): Context => {
|
||||
const ctx = useContext(flyoutPanelContext);
|
||||
|
||||
if (ctx === undefined) {
|
||||
throw new Error('useFlyoutPanel() must be used within a <flyoutPanelContext.Provider />');
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
.fieldEditor__flyoutPanels {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fieldEditor__flyoutPanel {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: $euiSizeL $euiSizeL 0 $euiSizeL;
|
||||
|
||||
&--pageBackground {
|
||||
background-color: $euiPageBackgroundColor;
|
||||
}
|
||||
&--emptyShade {
|
||||
background-color: $euiColorEmptyShade;
|
||||
}
|
||||
&--leftBorder {
|
||||
border-left: $euiBorderThin;
|
||||
}
|
||||
&--rightBorder {
|
||||
border-right: $euiBorderThin;
|
||||
}
|
||||
&--withContent {
|
||||
padding: 0;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $euiSizeL;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useState,
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useLayoutEffect,
|
||||
} from 'react';
|
||||
import { EuiFlexGroup, EuiFlexGroupProps } from '@elastic/eui';
|
||||
|
||||
import './flyout_panels.scss';
|
||||
|
||||
interface Panel {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface Context {
|
||||
addPanel: (panel: Panel) => () => void;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
|
||||
const panelId = () => idx++;
|
||||
|
||||
const flyoutPanelsContext = createContext<Context>({
|
||||
addPanel() {
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
export interface Props {
|
||||
/**
|
||||
* The total max width with all the panels in the DOM
|
||||
* Corresponds to the "maxWidth" prop passed to the EuiFlyout
|
||||
*/
|
||||
maxWidth: number;
|
||||
/** The className selector of the flyout */
|
||||
flyoutClassName: string;
|
||||
/** The size between the panels. Corresponds to EuiFlexGroup gutterSize */
|
||||
gutterSize?: EuiFlexGroupProps['gutterSize'];
|
||||
}
|
||||
|
||||
export const Panels: React.FC<Props> = ({ maxWidth, flyoutClassName, ...props }) => {
|
||||
const flyoutDOMelement = useMemo(() => {
|
||||
const el = document.getElementsByClassName(flyoutClassName);
|
||||
|
||||
if (el.length === 0) {
|
||||
// throw new Error(`Flyout with className "${flyoutClassName}" not found.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return el.item(0) as HTMLDivElement;
|
||||
}, [flyoutClassName]);
|
||||
|
||||
const [panels, setPanels] = useState<{ [id: number]: Panel }>({});
|
||||
|
||||
const removePanel = useCallback((id: number) => {
|
||||
setPanels((prev) => {
|
||||
const { [id]: panelToRemove, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const addPanel = useCallback(
|
||||
(panel: Panel) => {
|
||||
const nextId = panelId();
|
||||
setPanels((prev) => {
|
||||
return { ...prev, [nextId]: panel };
|
||||
});
|
||||
return removePanel.bind(null, nextId);
|
||||
},
|
||||
[removePanel]
|
||||
);
|
||||
|
||||
const ctx: Context = useMemo(
|
||||
() => ({
|
||||
addPanel,
|
||||
}),
|
||||
[addPanel]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!flyoutDOMelement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPercentWidth = Math.min(
|
||||
100,
|
||||
Object.values(panels).reduce((acc, { width = 0 }) => acc + width, 0)
|
||||
);
|
||||
const currentWidth = (maxWidth * totalPercentWidth) / 100;
|
||||
|
||||
flyoutDOMelement.style.maxWidth = `${currentWidth}px`;
|
||||
}, [panels, maxWidth, flyoutClassName, flyoutDOMelement]);
|
||||
|
||||
return (
|
||||
<flyoutPanelsContext.Provider value={ctx}>
|
||||
<EuiFlexGroup className="fieldEditor__flyoutPanels" gutterSize="none" {...props} />
|
||||
</flyoutPanelsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFlyoutPanelsContext = (): Context => {
|
||||
const ctx = useContext(flyoutPanelsContext);
|
||||
|
||||
if (ctx === undefined) {
|
||||
throw new Error('<Panel /> must be used within a <Panels /> wrapper');
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
|
@ -5,15 +5,16 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import React from 'react';
|
||||
import { LoadingState } from '../loading_state';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useFlyoutPanelContext } from './flyout_panel';
|
||||
|
||||
describe('LoadingState', () => {
|
||||
it('should render normally', () => {
|
||||
const component = shallow(<LoadingState />);
|
||||
export const PanelContent: React.FC = (props) => {
|
||||
const { registerContent } = useFlyoutPanelContext();
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
useEffect(() => {
|
||||
registerContent();
|
||||
}, [registerContent]);
|
||||
|
||||
return <div className="fieldEditor__flyoutPanel__content" {...props} />;
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import { EuiFlyoutFooter, EuiFlyoutFooterProps } from '@elastic/eui';
|
||||
|
||||
import { useFlyoutPanelContext } from './flyout_panel';
|
||||
|
||||
export const PanelFooter: React.FC<
|
||||
{ children: React.ReactNode } & Omit<EuiFlyoutFooterProps, 'children'>
|
||||
> = (props) => {
|
||||
const { registerFooter } = useFlyoutPanelContext();
|
||||
|
||||
useEffect(() => {
|
||||
registerFooter();
|
||||
}, [registerFooter]);
|
||||
|
||||
return <EuiFlyoutFooter className="fieldEditor__flyoutPanel__footer" {...props} />;
|
||||
};
|
|
@ -7,13 +7,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Header } from '../header';
|
||||
import { shallow } from 'enzyme';
|
||||
import { EuiSpacer, EuiFlyoutHeader, EuiFlyoutHeaderProps } from '@elastic/eui';
|
||||
|
||||
describe('Header', () => {
|
||||
it('should render normally', () => {
|
||||
const component = shallow(<Header indexPattern="ki*" indexPatternName="ki*" />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
export const PanelHeader: React.FunctionComponent<
|
||||
{ children: React.ReactNode } & Omit<EuiFlyoutHeaderProps, 'children'>
|
||||
> = (props) => (
|
||||
<>
|
||||
<EuiFlyoutHeader className="fieldEditor__flyoutPanel__header" {...props} />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PanelFooter } from './flyout_panels_footer';
|
||||
import { PanelHeader } from './flyout_panels_header';
|
||||
import { PanelContent } from './flyout_panels_content';
|
||||
import { Panel } from './flyout_panel';
|
||||
import { Panels } from './flyout_panels';
|
||||
|
||||
export const FlyoutPanels = {
|
||||
Group: Panels,
|
||||
Item: Panel,
|
||||
Content: PanelContent,
|
||||
Header: PanelHeader,
|
||||
Footer: PanelFooter,
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EuiFlyoutFooter,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface FooterProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: () => void;
|
||||
submitDisabled: boolean;
|
||||
}
|
||||
|
||||
const closeButtonLabel = i18n.translate('indexPatternEditor.editor.flyoutCloseButtonLabel', {
|
||||
defaultMessage: 'Close',
|
||||
});
|
||||
|
||||
const saveButtonLabel = i18n.translate('indexPatternEditor.editor.flyoutSaveButtonLabel', {
|
||||
defaultMessage: 'Create index pattern',
|
||||
});
|
||||
|
||||
export const Footer = ({ onCancel, onSubmit, submitDisabled }: FooterProps) => {
|
||||
return (
|
||||
<EuiFlyoutFooter className="indexPatternEditor__footer">
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={onCancel}
|
||||
data-test-subj="closeFlyoutButton"
|
||||
>
|
||||
{closeButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
data-test-subj="saveIndexPatternButton"
|
||||
fill
|
||||
disabled={submitDisabled}
|
||||
>
|
||||
{saveButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
);
|
||||
};
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { Header } from './header';
|
||||
export { Footer } from './footer';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { TimestampField } from './timestamp_field';
|
||||
export { TypeField } from './type_field';
|
||||
export { TitleField } from './title_field';
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFormRow, EuiComboBox, EuiFormHelpText, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
UseField,
|
||||
FieldConfig,
|
||||
ValidationConfig,
|
||||
getFieldValidityAndErrorMessage,
|
||||
} from '../../shared_imports';
|
||||
|
||||
import { TimestampOption } from '../../types';
|
||||
import { schema } from '../form_schema';
|
||||
|
||||
interface Props {
|
||||
options: TimestampOption[];
|
||||
isLoadingOptions: boolean;
|
||||
isExistingIndexPattern: boolean;
|
||||
isLoadingMatchedIndices: boolean;
|
||||
hasMatchedIndices: boolean;
|
||||
}
|
||||
|
||||
const requireTimestampOptionValidator = (options: Props['options']): ValidationConfig => ({
|
||||
validator: async ({ value }) => {
|
||||
const isValueRequired = !!options.length;
|
||||
if (isValueRequired && !value) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'indexPatternEditor.requireTimestampOption.ValidationErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Select a timestamp field.',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getTimestampConfig = (
|
||||
options: Props['options']
|
||||
): FieldConfig<EuiComboBoxOptionOption<string>> => {
|
||||
const timestampFieldConfig = schema.timestampField;
|
||||
|
||||
const validations = [
|
||||
...timestampFieldConfig.validations,
|
||||
// note this is responsible for triggering the state update for the selected source list.
|
||||
requireTimestampOptionValidator(options),
|
||||
];
|
||||
|
||||
return {
|
||||
...timestampFieldConfig!,
|
||||
validations,
|
||||
};
|
||||
};
|
||||
|
||||
const noTimestampOptionText = i18n.translate('indexPatternEditor.editor.form.noTimeFieldsLabel', {
|
||||
defaultMessage: 'No matching data stream, index, or alias has a timestamp field.',
|
||||
});
|
||||
|
||||
const timestampFieldHelp = i18n.translate('indexPatternEditor.editor.form.timeFieldHelp', {
|
||||
defaultMessage: 'Select a timestamp field for use with the global time filter.',
|
||||
});
|
||||
|
||||
export const TimestampField = ({
|
||||
options = [],
|
||||
isLoadingOptions = false,
|
||||
isExistingIndexPattern,
|
||||
isLoadingMatchedIndices,
|
||||
hasMatchedIndices,
|
||||
}: Props) => {
|
||||
const optionsAsComboBoxOptions = options.map(({ display, fieldName }) => ({
|
||||
label: display,
|
||||
value: fieldName,
|
||||
}));
|
||||
const timestampConfig = useMemo(() => getTimestampConfig(options), [options]);
|
||||
const selectTimestampHelp = options.length ? timestampFieldHelp : '';
|
||||
|
||||
const timestampNoFieldsHelp =
|
||||
options.length === 0 &&
|
||||
!isExistingIndexPattern &&
|
||||
!isLoadingMatchedIndices &&
|
||||
!isLoadingOptions &&
|
||||
hasMatchedIndices
|
||||
? noTimestampOptionText
|
||||
: '';
|
||||
|
||||
return (
|
||||
<UseField<EuiComboBoxOptionOption<string>> config={timestampConfig} path="timestampField">
|
||||
{(field) => {
|
||||
const { label, value, setValue } = field;
|
||||
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const isDisabled = !optionsAsComboBoxOptions.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={label}
|
||||
error={isDisabled ? null : errorMessage}
|
||||
isInvalid={!isDisabled && isInvalid}
|
||||
fullWidth
|
||||
>
|
||||
<>
|
||||
<EuiComboBox<string>
|
||||
placeholder={i18n.translate(
|
||||
'indexPatternEditor.editor.form.runtimeType.placeholderLabel',
|
||||
{
|
||||
defaultMessage: 'Select a timestamp field',
|
||||
}
|
||||
)}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={optionsAsComboBoxOptions}
|
||||
selectedOptions={value ? [value] : undefined}
|
||||
onChange={(newValue) => {
|
||||
if (newValue.length === 0) {
|
||||
// Don't allow clearing the type. One must always be selected
|
||||
return;
|
||||
}
|
||||
//
|
||||
setValue(newValue[0]);
|
||||
}}
|
||||
isClearable={false}
|
||||
isDisabled={isDisabled}
|
||||
data-test-subj="timestampField"
|
||||
aria-label={i18n.translate(
|
||||
'indexPatternEditor.editor.form.timestampSelectAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Timestamp field',
|
||||
}
|
||||
)}
|
||||
isLoading={isLoadingOptions}
|
||||
fullWidth
|
||||
/>
|
||||
<EuiFormHelpText>
|
||||
{timestampNoFieldsHelp || selectTimestampHelp || <> </>}
|
||||
</EuiFormHelpText>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, useState, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
|
||||
import {
|
||||
UseField,
|
||||
getFieldValidityAndErrorMessage,
|
||||
ValidationConfig,
|
||||
FieldConfig,
|
||||
} from '../../shared_imports';
|
||||
import { canAppendWildcard } from '../../lib';
|
||||
import { schema } from '../form_schema';
|
||||
import {
|
||||
MatchedItem,
|
||||
RollupIndicesCapsResponse,
|
||||
IndexPatternConfig,
|
||||
MatchedIndicesSet,
|
||||
} from '../../types';
|
||||
|
||||
interface RefreshMatchedIndicesResult {
|
||||
matchedIndicesResult: MatchedIndicesSet;
|
||||
newRollupIndexName?: string;
|
||||
}
|
||||
|
||||
interface TitleFieldProps {
|
||||
existingIndexPatterns: string[];
|
||||
isRollup: boolean;
|
||||
matchedIndices: MatchedItem[];
|
||||
rollupIndicesCapabilities: RollupIndicesCapsResponse;
|
||||
refreshMatchedIndices: (title: string) => Promise<RefreshMatchedIndicesResult>;
|
||||
}
|
||||
|
||||
const rollupIndexPatternNoMatchError = {
|
||||
message: i18n.translate('indexPatternEditor.rollupIndexPattern.createIndex.noMatchError', {
|
||||
defaultMessage: 'Rollup index pattern error: must match one rollup index',
|
||||
}),
|
||||
};
|
||||
|
||||
const rollupIndexPatternTooManyMatchesError = {
|
||||
message: i18n.translate('indexPatternEditor.rollupIndexPattern.createIndex.tooManyMatchesError', {
|
||||
defaultMessage: 'Rollup index pattern error: can only match one rollup index',
|
||||
}),
|
||||
};
|
||||
|
||||
const mustMatchError = {
|
||||
message: i18n.translate('indexPatternEditor.createIndex.noMatch', {
|
||||
defaultMessage: 'Name must match one or more data streams, indices, or aliases.',
|
||||
}),
|
||||
};
|
||||
|
||||
const createTitlesNoDupesValidator = (
|
||||
namesNotAllowed: string[]
|
||||
): ValidationConfig<{}, string, string> => ({
|
||||
validator: ({ value }) => {
|
||||
if (namesNotAllowed.includes(value)) {
|
||||
return {
|
||||
message: i18n.translate('indexPatternEditor.indexPatternExists.ValidationErrorMessage', {
|
||||
defaultMessage: 'An index pattern with this title already exists.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
interface MatchesValidatorArgs {
|
||||
rollupIndicesCapabilities: Record<string, { error: string }>;
|
||||
refreshMatchedIndices: (title: string) => Promise<RefreshMatchedIndicesResult>;
|
||||
isRollup: boolean;
|
||||
}
|
||||
|
||||
const createMatchesIndicesValidator = ({
|
||||
rollupIndicesCapabilities,
|
||||
refreshMatchedIndices,
|
||||
isRollup,
|
||||
}: MatchesValidatorArgs): ValidationConfig<{}, string, string> => ({
|
||||
validator: async ({ value }) => {
|
||||
const { matchedIndicesResult, newRollupIndexName } = await refreshMatchedIndices(value);
|
||||
const rollupIndices = Object.keys(rollupIndicesCapabilities);
|
||||
|
||||
if (matchedIndicesResult.exactMatchedIndices.length === 0) {
|
||||
return mustMatchError;
|
||||
}
|
||||
|
||||
if (!isRollup || !rollupIndices || !rollupIndices.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A rollup index pattern needs to match one and only one rollup index.
|
||||
const rollupIndexMatches = matchedIndicesResult.exactMatchedIndices.filter((matchedIndex) =>
|
||||
rollupIndices.includes(matchedIndex.name)
|
||||
);
|
||||
|
||||
if (!rollupIndexMatches.length) {
|
||||
return rollupIndexPatternNoMatchError;
|
||||
} else if (rollupIndexMatches.length > 1) {
|
||||
return rollupIndexPatternTooManyMatchesError;
|
||||
}
|
||||
|
||||
// Error info is potentially provided via the rollup indices caps request
|
||||
const error = newRollupIndexName && rollupIndicesCapabilities[newRollupIndexName].error;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
message: i18n.translate('indexPatternEditor.rollup.uncaughtError', {
|
||||
defaultMessage: 'Rollup index pattern error: {error}',
|
||||
values: {
|
||||
error,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
interface GetTitleConfigArgs {
|
||||
namesNotAllowed: string[];
|
||||
isRollup: boolean;
|
||||
matchedIndices: MatchedItem[];
|
||||
rollupIndicesCapabilities: RollupIndicesCapsResponse;
|
||||
refreshMatchedIndices: (title: string) => Promise<RefreshMatchedIndicesResult>;
|
||||
}
|
||||
|
||||
const getTitleConfig = ({
|
||||
namesNotAllowed,
|
||||
isRollup,
|
||||
rollupIndicesCapabilities,
|
||||
refreshMatchedIndices,
|
||||
}: GetTitleConfigArgs): FieldConfig<string> => {
|
||||
const titleFieldConfig = schema.title;
|
||||
|
||||
const validations = [
|
||||
...titleFieldConfig.validations,
|
||||
// note this is responsible for triggering the state update for the selected source list.
|
||||
createMatchesIndicesValidator({
|
||||
rollupIndicesCapabilities,
|
||||
refreshMatchedIndices,
|
||||
isRollup,
|
||||
}),
|
||||
createTitlesNoDupesValidator(namesNotAllowed),
|
||||
];
|
||||
|
||||
return {
|
||||
...titleFieldConfig!,
|
||||
validations,
|
||||
};
|
||||
};
|
||||
|
||||
export const TitleField = ({
|
||||
existingIndexPatterns,
|
||||
isRollup,
|
||||
matchedIndices,
|
||||
rollupIndicesCapabilities,
|
||||
refreshMatchedIndices,
|
||||
}: TitleFieldProps) => {
|
||||
const [appendedWildcard, setAppendedWildcard] = useState<boolean>(false);
|
||||
|
||||
const fieldConfig = useMemo(
|
||||
() =>
|
||||
getTitleConfig({
|
||||
namesNotAllowed: existingIndexPatterns,
|
||||
isRollup,
|
||||
matchedIndices,
|
||||
rollupIndicesCapabilities,
|
||||
refreshMatchedIndices,
|
||||
}),
|
||||
[
|
||||
existingIndexPatterns,
|
||||
isRollup,
|
||||
matchedIndices,
|
||||
rollupIndicesCapabilities,
|
||||
refreshMatchedIndices,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<UseField<string, IndexPatternConfig>
|
||||
path="title"
|
||||
config={fieldConfig}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'aria-label': i18n.translate('indexPatternEditor.form.titleAriaLabel', {
|
||||
defaultMessage: 'Title field',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(field) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={typeof field.helpText === 'function' ? field.helpText() : field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={isInvalid}
|
||||
value={field.value}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.persist();
|
||||
let query = e.target.value;
|
||||
if (query.length === 1 && !appendedWildcard && canAppendWildcard(query)) {
|
||||
query += '*';
|
||||
setAppendedWildcard(true);
|
||||
setTimeout(() => e.target.setSelectionRange(1, 1));
|
||||
} else {
|
||||
if (['', '*'].includes(query) && appendedWildcard) {
|
||||
query = '';
|
||||
setAppendedWildcard(false);
|
||||
}
|
||||
}
|
||||
field.setValue(query);
|
||||
}}
|
||||
isLoading={field.isValidating}
|
||||
fullWidth
|
||||
data-test-subj="createIndexPatternNameInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// @ts-ignore
|
||||
import { euiColorAccent } from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiSuperSelect,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiBadge,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { UseField } from '../../shared_imports';
|
||||
|
||||
import { INDEX_PATTERN_TYPE, IndexPatternConfig } from '../../types';
|
||||
|
||||
interface TypeFieldProps {
|
||||
onChange: (type: INDEX_PATTERN_TYPE) => void;
|
||||
}
|
||||
|
||||
const standardSelectItem = (
|
||||
<EuiDescriptionList style={{ whiteSpace: 'nowrap' }} data-test-subj="standardType">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="indexPatternEditor.typeSelect.standardTitle"
|
||||
defaultMessage="Standard index pattern"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<FormattedMessage
|
||||
id="indexPatternEditor.typeSelect.standardDescription"
|
||||
defaultMessage="Perform full aggregations against any data"
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
);
|
||||
|
||||
const rollupSelectItem = (
|
||||
<EuiDescriptionList style={{ whiteSpace: 'nowrap' }} data-test-subj="rollupType">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="indexPatternEditor.typeSelect.rollupTitle"
|
||||
defaultMessage="Rollup index pattern"
|
||||
/>
|
||||
|
||||
<EuiBadge color={euiColorAccent}>
|
||||
<FormattedMessage id="indexPatternEditor.typeSelect.betaLabel" defaultMessage="Beta" />
|
||||
</EuiBadge>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<FormattedMessage
|
||||
id="indexPatternEditor.typeSelect.rollupDescription"
|
||||
defaultMessage="Perform limited aggregations against summarized data"
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
);
|
||||
|
||||
export const TypeField = ({ onChange }: TypeFieldProps) => {
|
||||
return (
|
||||
<UseField<INDEX_PATTERN_TYPE, IndexPatternConfig> path="type">
|
||||
{({ label, value, setValue }) => {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow label={label} fullWidth>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="typeField"
|
||||
options={[
|
||||
{
|
||||
value: INDEX_PATTERN_TYPE.DEFAULT,
|
||||
inputDisplay: i18n.translate('indexPatternEditor.typeSelect.standard', {
|
||||
defaultMessage: 'Standard',
|
||||
}),
|
||||
dropdownDisplay: standardSelectItem,
|
||||
},
|
||||
{
|
||||
value: INDEX_PATTERN_TYPE.ROLLUP,
|
||||
inputDisplay: i18n.translate('indexPatternEditor.typeSelect.rollup', {
|
||||
defaultMessage: 'Rollup',
|
||||
}),
|
||||
dropdownDisplay: rollupSelectItem,
|
||||
},
|
||||
]}
|
||||
valueOfSelected={value}
|
||||
onChange={(newValue) => {
|
||||
setValue(newValue);
|
||||
onChange(newValue);
|
||||
}}
|
||||
aria-label={i18n.translate('indexPatternEditor.editor.form.typeSelectAriaLabel', {
|
||||
defaultMessage: 'Type field',
|
||||
})}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { fieldValidators } from '../shared_imports';
|
||||
import { INDEX_PATTERN_TYPE } from '../types';
|
||||
|
||||
export const schema = {
|
||||
title: {
|
||||
label: i18n.translate('indexPatternEditor.editor.form.titleLabel', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
defaultValue: '',
|
||||
helpText: i18n.translate('indexPatternEditor.validations.titleHelpText', {
|
||||
defaultMessage:
|
||||
'Use an asterisk (*) to match multiple indices. Spaces and the characters , /, ?, ", <, >, | are not allowed.',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.emptyField(
|
||||
i18n.translate('indexPatternEditor.validations.titleIsRequiredErrorMessage', {
|
||||
defaultMessage: 'A name is required.',
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
timestampField: {
|
||||
label: i18n.translate('indexPatternEditor.editor.form.timeFieldLabel', {
|
||||
defaultMessage: 'Timestamp field',
|
||||
}),
|
||||
helpText: i18n.translate('indexPatternEditor.editor.form.timestampFieldHelp', {
|
||||
defaultMessage: 'Select a timestamp field for use with the global time filter.',
|
||||
}),
|
||||
validations: [],
|
||||
},
|
||||
allowHidden: {
|
||||
label: i18n.translate('indexPatternEditor.editor.form.allowHiddenLabel', {
|
||||
defaultMessage: 'Allow hidden and system indices',
|
||||
}),
|
||||
defaultValue: false,
|
||||
},
|
||||
id: {
|
||||
label: i18n.translate('indexPatternEditor.editor.form.customIdLabel', {
|
||||
defaultMessage: 'Custom index pattern ID',
|
||||
}),
|
||||
helpText: i18n.translate('indexPatternEditor.editor.form.customIdHelp', {
|
||||
defaultMessage:
|
||||
'Kibana provides a unique identifier for each index pattern, or you can create your own.',
|
||||
}),
|
||||
},
|
||||
type: {
|
||||
label: i18n.translate('indexPatternEditor.editor.form.TypeLabel', {
|
||||
defaultMessage: 'Index pattern type',
|
||||
}),
|
||||
defaultValue: INDEX_PATTERN_TYPE.DEFAULT,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const geti18nTexts = () => {
|
||||
return {
|
||||
noTimestampOptionText: i18n.translate(
|
||||
'indexPatternEditor.createIndexPattern.stepTime.noTimeFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'No matching data stream, index, or alias has a timestamp field.',
|
||||
}
|
||||
),
|
||||
timestampFieldHelp: i18n.translate('indexPatternEditor.editor.form.timeFieldHelp', {
|
||||
defaultMessage: 'Select a timestamp field for use with the global time filter.',
|
||||
}),
|
||||
rollupLabel: i18n.translate('indexPatternEditor.rollupIndexPattern.createIndex.indexLabel', {
|
||||
defaultMessage: 'Rollup',
|
||||
}),
|
||||
};
|
||||
};
|
24
src/plugins/index_pattern_editor/public/components/index.ts
Normal file
24
src/plugins/index_pattern_editor/public/components/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export {
|
||||
IndexPatternEditorFlyoutContent,
|
||||
Props as IndexPatternEditorFlyoutContentProps,
|
||||
} from './index_pattern_editor_flyout_content';
|
||||
|
||||
export { IndexPatternEditor } from './index_pattern_editor';
|
||||
|
||||
export { schema } from './form_schema';
|
||||
export { TimestampField, TypeField, TitleField } from './form_fields';
|
||||
export { EmptyPrompts } from './empty_prompts';
|
||||
export { PreviewPanel } from './preview_panel';
|
||||
export { LoadingIndices } from './loading_indices';
|
||||
export { geti18nTexts } from './i18n_texts';
|
||||
export { Footer } from './footer';
|
||||
export { AdvancedParamsContent } from './advanced_params_content';
|
||||
export { RollupBetaWarning } from './rollup_beta_warning';
|
|
@ -0,0 +1,13 @@
|
|||
.indexPatternEditor__form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.fieldEditor__mainFlyoutPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.indexPatternEditor__footer {
|
||||
margin-left: -$euiSizeL;
|
||||
margin-right: -$euiSizeL;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlyout } from '@elastic/eui';
|
||||
import { IndexPatternEditorLazy } from './index_pattern_editor_lazy';
|
||||
import { IndexPatternEditorContext, IndexPatternEditorProps } from '../types';
|
||||
import { createKibanaReactContext } from '../shared_imports';
|
||||
import './index_pattern_editor.scss';
|
||||
|
||||
export interface IndexPatternEditorPropsWithServices extends IndexPatternEditorProps {
|
||||
services: IndexPatternEditorContext;
|
||||
}
|
||||
|
||||
export const IndexPatternEditor = ({
|
||||
onSave,
|
||||
onCancel = () => {},
|
||||
services,
|
||||
defaultTypeIsRollup = false,
|
||||
requireTimestampField = false,
|
||||
}: IndexPatternEditorPropsWithServices) => {
|
||||
const {
|
||||
Provider: KibanaReactContextProvider,
|
||||
} = createKibanaReactContext<IndexPatternEditorContext>(services);
|
||||
|
||||
return (
|
||||
<KibanaReactContextProvider>
|
||||
<EuiFlyout onClose={() => {}} hideCloseButton={true} size="l">
|
||||
<IndexPatternEditorLazy
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
defaultTypeIsRollup={defaultTypeIsRollup}
|
||||
requireTimestampField={requireTimestampField}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</KibanaReactContextProvider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,379 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
IndexPatternSpec,
|
||||
Form,
|
||||
useForm,
|
||||
useFormData,
|
||||
useKibana,
|
||||
GetFieldsOptions,
|
||||
} from '../shared_imports';
|
||||
|
||||
import { ensureMinimumTime, getIndices, extractTimeFields, getMatchedIndices } from '../lib';
|
||||
import { FlyoutPanels } from './flyout_panels';
|
||||
|
||||
import {
|
||||
MatchedItem,
|
||||
IndexPatternEditorContext,
|
||||
RollupIndicesCapsResponse,
|
||||
INDEX_PATTERN_TYPE,
|
||||
IndexPatternConfig,
|
||||
MatchedIndicesSet,
|
||||
FormInternal,
|
||||
TimestampOption,
|
||||
} from '../types';
|
||||
|
||||
import {
|
||||
TimestampField,
|
||||
TypeField,
|
||||
TitleField,
|
||||
schema,
|
||||
Footer,
|
||||
AdvancedParamsContent,
|
||||
EmptyPrompts,
|
||||
PreviewPanel,
|
||||
RollupBetaWarning,
|
||||
} from '.';
|
||||
|
||||
export interface Props {
|
||||
/**
|
||||
* Handler for the "save" footer button
|
||||
*/
|
||||
onSave: (indexPatternSpec: IndexPatternSpec) => void;
|
||||
/**
|
||||
* Handler for the "cancel" footer button
|
||||
*/
|
||||
onCancel: () => void;
|
||||
defaultTypeIsRollup?: boolean;
|
||||
requireTimestampField?: boolean;
|
||||
}
|
||||
|
||||
const editorTitle = i18n.translate('indexPatternEditor.title', {
|
||||
defaultMessage: 'Create index pattern',
|
||||
});
|
||||
|
||||
const IndexPatternEditorFlyoutContentComponent = ({
|
||||
onSave,
|
||||
onCancel,
|
||||
defaultTypeIsRollup,
|
||||
requireTimestampField = false,
|
||||
}: Props) => {
|
||||
const isMounted = useRef<boolean>(false);
|
||||
const {
|
||||
services: { http, indexPatternService, uiSettings },
|
||||
} = useKibana<IndexPatternEditorContext>();
|
||||
|
||||
const { form } = useForm<IndexPatternConfig, FormInternal>({
|
||||
defaultValue: {
|
||||
type: defaultTypeIsRollup ? INDEX_PATTERN_TYPE.ROLLUP : INDEX_PATTERN_TYPE.DEFAULT,
|
||||
},
|
||||
schema,
|
||||
onSubmit: async (formData, isValid) => {
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexPatternStub: IndexPatternSpec = {
|
||||
title: formData.title,
|
||||
timeFieldName: formData.timestampField?.value,
|
||||
id: formData.id,
|
||||
};
|
||||
|
||||
if (type === INDEX_PATTERN_TYPE.ROLLUP && rollupIndex) {
|
||||
indexPatternStub.type = INDEX_PATTERN_TYPE.ROLLUP;
|
||||
indexPatternStub.typeMeta = {
|
||||
params: {
|
||||
rollup_index: rollupIndex,
|
||||
},
|
||||
aggs: rollupIndicesCapabilities[rollupIndex].aggs,
|
||||
};
|
||||
}
|
||||
|
||||
await onSave(indexPatternStub);
|
||||
},
|
||||
});
|
||||
|
||||
const { getFields } = form;
|
||||
|
||||
const [{ title, allowHidden, type }] = useFormData<FormInternal>({ form });
|
||||
const [isLoadingSources, setIsLoadingSources] = useState<boolean>(true);
|
||||
|
||||
const [timestampFieldOptions, setTimestampFieldOptions] = useState<TimestampOption[]>([]);
|
||||
const [isLoadingTimestampFields, setIsLoadingTimestampFields] = useState<boolean>(false);
|
||||
const [isLoadingMatchedIndices, setIsLoadingMatchedIndices] = useState<boolean>(false);
|
||||
const [allSources, setAllSources] = useState<MatchedItem[]>([]);
|
||||
const [isLoadingIndexPatterns, setIsLoadingIndexPatterns] = useState<boolean>(true);
|
||||
const [existingIndexPatterns, setExistingIndexPatterns] = useState<string[]>([]);
|
||||
const [rollupIndex, setRollupIndex] = useState<string | undefined>();
|
||||
const [
|
||||
rollupIndicesCapabilities,
|
||||
setRollupIndicesCapabilities,
|
||||
] = useState<RollupIndicesCapsResponse>({});
|
||||
const [matchedIndices, setMatchedIndices] = useState<MatchedIndicesSet>({
|
||||
allIndices: [],
|
||||
exactMatchedIndices: [],
|
||||
partialMatchedIndices: [],
|
||||
visibleIndices: [],
|
||||
});
|
||||
|
||||
// load all data sources and set initial matchedIndices
|
||||
const loadSources = useCallback(() => {
|
||||
getIndices(http, () => false, '*', allowHidden).then((dataSources) => {
|
||||
setAllSources(dataSources);
|
||||
const matchedSet = getMatchedIndices(dataSources, [], [], allowHidden);
|
||||
setMatchedIndices(matchedSet);
|
||||
setIsLoadingSources(false);
|
||||
});
|
||||
}, [http, allowHidden]);
|
||||
|
||||
// loading list of index patterns
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
loadSources();
|
||||
const getTitles = async () => {
|
||||
const indexPatternTitles = await indexPatternService.getTitles();
|
||||
if (isMounted.current) {
|
||||
setExistingIndexPatterns(indexPatternTitles);
|
||||
setIsLoadingIndexPatterns(false);
|
||||
}
|
||||
};
|
||||
getTitles();
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [http, indexPatternService, loadSources]);
|
||||
|
||||
// loading rollup info
|
||||
useEffect(() => {
|
||||
const getRollups = async () => {
|
||||
try {
|
||||
const response = await http.get('/api/rollup/indices');
|
||||
if (isMounted.current) {
|
||||
setRollupIndicesCapabilities(response || {});
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently swallow failure responses such as expired trials
|
||||
}
|
||||
};
|
||||
|
||||
getRollups();
|
||||
}, [http, type]);
|
||||
|
||||
const getRollupIndices = (rollupCaps: RollupIndicesCapsResponse) => Object.keys(rollupCaps);
|
||||
|
||||
const loadTimestampFieldOptions = useCallback(
|
||||
async (query: string) => {
|
||||
let timestampOptions: TimestampOption[] = [];
|
||||
const isValidResult =
|
||||
!existingIndexPatterns.includes(query) && matchedIndices.exactMatchedIndices.length > 0;
|
||||
if (isValidResult) {
|
||||
setIsLoadingTimestampFields(true);
|
||||
const getFieldsOptions: GetFieldsOptions = {
|
||||
pattern: query,
|
||||
};
|
||||
if (type === INDEX_PATTERN_TYPE.ROLLUP) {
|
||||
getFieldsOptions.type = INDEX_PATTERN_TYPE.ROLLUP;
|
||||
getFieldsOptions.rollupIndex = rollupIndex;
|
||||
}
|
||||
|
||||
const fields = await ensureMinimumTime(
|
||||
indexPatternService.getFieldsForWildcard(getFieldsOptions)
|
||||
);
|
||||
timestampOptions = extractTimeFields(fields, requireTimestampField);
|
||||
}
|
||||
if (isMounted.current) {
|
||||
setIsLoadingTimestampFields(false);
|
||||
setTimestampFieldOptions(timestampOptions);
|
||||
}
|
||||
return timestampOptions;
|
||||
},
|
||||
[
|
||||
existingIndexPatterns,
|
||||
indexPatternService,
|
||||
requireTimestampField,
|
||||
rollupIndex,
|
||||
type,
|
||||
matchedIndices.exactMatchedIndices,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadTimestampFieldOptions(title);
|
||||
getFields().timestampField?.setValue('');
|
||||
}, [matchedIndices, loadTimestampFieldOptions, title, getFields]);
|
||||
|
||||
const reloadMatchedIndices = useCallback(
|
||||
async (newTitle: string) => {
|
||||
const isRollupIndex = (indexName: string) =>
|
||||
getRollupIndices(rollupIndicesCapabilities).includes(indexName);
|
||||
let newRollupIndexName: string | undefined;
|
||||
|
||||
const fetchIndices = async (query: string = '') => {
|
||||
setIsLoadingMatchedIndices(true);
|
||||
const indexRequests = [];
|
||||
|
||||
if (query?.endsWith('*')) {
|
||||
const exactMatchedQuery = getIndices(http, isRollupIndex, query, allowHidden);
|
||||
indexRequests.push(exactMatchedQuery);
|
||||
// provide default value when not making a request for the partialMatchQuery
|
||||
indexRequests.push(Promise.resolve([]));
|
||||
} else {
|
||||
const exactMatchQuery = getIndices(http, isRollupIndex, query, allowHidden);
|
||||
const partialMatchQuery = getIndices(http, isRollupIndex, `${query}*`, allowHidden);
|
||||
|
||||
indexRequests.push(exactMatchQuery);
|
||||
indexRequests.push(partialMatchQuery);
|
||||
}
|
||||
|
||||
const [exactMatched, partialMatched] = (await ensureMinimumTime(
|
||||
indexRequests
|
||||
)) as MatchedItem[][];
|
||||
|
||||
const matchedIndicesResult = getMatchedIndices(
|
||||
allSources,
|
||||
partialMatched,
|
||||
exactMatched,
|
||||
allowHidden
|
||||
);
|
||||
|
||||
if (isMounted.current) {
|
||||
if (type === INDEX_PATTERN_TYPE.ROLLUP) {
|
||||
const rollupIndices = exactMatched.filter((index) => isRollupIndex(index.name));
|
||||
newRollupIndexName = rollupIndices.length === 1 ? rollupIndices[0].name : undefined;
|
||||
setRollupIndex(newRollupIndexName);
|
||||
} else {
|
||||
setRollupIndex(undefined);
|
||||
}
|
||||
|
||||
setMatchedIndices(matchedIndicesResult);
|
||||
setIsLoadingMatchedIndices(false);
|
||||
}
|
||||
|
||||
return { matchedIndicesResult, newRollupIndexName };
|
||||
};
|
||||
|
||||
return fetchIndices(newTitle);
|
||||
},
|
||||
[http, allowHidden, allSources, type, rollupIndicesCapabilities]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
reloadMatchedIndices(title);
|
||||
}, [allowHidden, reloadMatchedIndices, title]);
|
||||
|
||||
const onTypeChange = useCallback(
|
||||
(newType) => {
|
||||
form.setFieldValue('title', '');
|
||||
form.setFieldValue('timestampField', '');
|
||||
if (newType === INDEX_PATTERN_TYPE.ROLLUP) {
|
||||
form.setFieldValue('allowHidden', false);
|
||||
}
|
||||
},
|
||||
[form]
|
||||
);
|
||||
|
||||
if (isLoadingSources || isLoadingIndexPatterns) {
|
||||
return <EuiLoadingSpinner size="xl" />;
|
||||
}
|
||||
|
||||
const showIndexPatternTypeSelect = () =>
|
||||
uiSettings.isDeclared('rollups:enableIndexPatterns') &&
|
||||
uiSettings.get('rollups:enableIndexPatterns') &&
|
||||
getRollupIndices(rollupIndicesCapabilities).length;
|
||||
|
||||
const indexPatternTypeSelect = showIndexPatternTypeSelect() ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<TypeField onChange={onTypeChange} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{type === INDEX_PATTERN_TYPE.ROLLUP ? (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<RollupBetaWarning />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
return (
|
||||
<EmptyPrompts
|
||||
onCancel={onCancel}
|
||||
allSources={allSources}
|
||||
hasExistingIndexPatterns={!!existingIndexPatterns.length}
|
||||
loadSources={loadSources}
|
||||
>
|
||||
<FlyoutPanels.Group flyoutClassName={'indexPatternEditorFlyout'} maxWidth={1180}>
|
||||
<FlyoutPanels.Item className="fieldEditor__mainFlyoutPanel" border="right">
|
||||
<EuiTitle data-test-subj="flyoutTitle">
|
||||
<h2>{editorTitle}</h2>
|
||||
</EuiTitle>
|
||||
<Form form={form} className="indexPatternEditor__form">
|
||||
{indexPatternTypeSelect}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<TitleField
|
||||
isRollup={form.getFields().type?.value === INDEX_PATTERN_TYPE.ROLLUP}
|
||||
existingIndexPatterns={existingIndexPatterns}
|
||||
refreshMatchedIndices={reloadMatchedIndices}
|
||||
matchedIndices={matchedIndices.exactMatchedIndices}
|
||||
rollupIndicesCapabilities={rollupIndicesCapabilities}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<TimestampField
|
||||
options={timestampFieldOptions}
|
||||
isLoadingOptions={isLoadingTimestampFields}
|
||||
isExistingIndexPattern={existingIndexPatterns.includes(title)}
|
||||
isLoadingMatchedIndices={isLoadingMatchedIndices}
|
||||
hasMatchedIndices={!!matchedIndices.exactMatchedIndices.length}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<AdvancedParamsContent disableAllowHidden={type === INDEX_PATTERN_TYPE.ROLLUP} />
|
||||
</Form>
|
||||
<Footer
|
||||
onCancel={onCancel}
|
||||
onSubmit={() => form.submit()}
|
||||
submitDisabled={form.isSubmitted && !form.isValid}
|
||||
/>
|
||||
</FlyoutPanels.Item>
|
||||
<FlyoutPanels.Item>
|
||||
{isLoadingSources ? (
|
||||
<></>
|
||||
) : (
|
||||
<PreviewPanel
|
||||
type={type}
|
||||
allowHidden={allowHidden}
|
||||
title={title}
|
||||
matched={matchedIndices}
|
||||
/>
|
||||
)}
|
||||
</FlyoutPanels.Item>
|
||||
</FlyoutPanels.Group>
|
||||
</EmptyPrompts>
|
||||
);
|
||||
};
|
||||
|
||||
export const IndexPatternEditorFlyoutContent = React.memo(IndexPatternEditorFlyoutContentComponent);
|
|
@ -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
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { IndexPatternEditorProps } from '../types';
|
||||
|
||||
const IndexPatternFlyoutContentContainer = lazy(
|
||||
() => import('./index_pattern_flyout_content_container')
|
||||
);
|
||||
|
||||
export const IndexPatternEditorLazy = (props: IndexPatternEditorProps) => (
|
||||
<Suspense fallback={<EuiLoadingSpinner size="xl" />}>
|
||||
<IndexPatternFlyoutContentContainer {...props} />
|
||||
</Suspense>
|
||||
);
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { IndexPatternSpec, useKibana } from '../shared_imports';
|
||||
import { IndexPatternEditorFlyoutContent } from './index_pattern_editor_flyout_content';
|
||||
import { IndexPatternEditorContext, IndexPatternEditorProps } from '../types';
|
||||
|
||||
const IndexPatternFlyoutContentContainer = ({
|
||||
onSave,
|
||||
onCancel = () => {},
|
||||
defaultTypeIsRollup,
|
||||
requireTimestampField = false,
|
||||
}: IndexPatternEditorProps) => {
|
||||
const {
|
||||
services: { indexPatternService, notifications },
|
||||
} = useKibana<IndexPatternEditorContext>();
|
||||
|
||||
const onSaveClick = async (indexPatternSpec: IndexPatternSpec) => {
|
||||
try {
|
||||
const indexPattern = await indexPatternService.createAndSave(indexPatternSpec);
|
||||
|
||||
const message = i18n.translate('indexPatternEditor.saved', {
|
||||
defaultMessage: "Saved '{indexPatternTitle}'",
|
||||
values: { indexPatternTitle: indexPattern.title },
|
||||
});
|
||||
notifications.toasts.addSuccess(message);
|
||||
await onSave(indexPattern);
|
||||
} catch (e) {
|
||||
const title = i18n.translate('indexPatternEditor.indexPatterns.unableSaveLabel', {
|
||||
defaultMessage: 'Failed to save index pattern.',
|
||||
});
|
||||
|
||||
notifications.toasts.addDanger({ title });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IndexPatternEditorFlyoutContent
|
||||
onSave={onSaveClick}
|
||||
onCancel={onCancel}
|
||||
defaultTypeIsRollup={defaultTypeIsRollup}
|
||||
requireTimestampField={requireTimestampField}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/* eslint-disable import/no-default-export */
|
||||
export default IndexPatternFlyoutContentContainer;
|
|
@ -18,7 +18,7 @@ exports[`LoadingIndices should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Looking for matching indices…"
|
||||
id="indexPatternManagement.createIndexPattern.step.loadingHeader"
|
||||
id="indexPatternEditor.loadingHeader"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h3>
|
|
@ -24,7 +24,7 @@ export const LoadingIndices = ({ ...rest }) => (
|
|||
<EuiTitle size="s">
|
||||
<h3 className="eui-textCenter">
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.loadingHeader"
|
||||
id="indexPatternEditor.loadingHeader"
|
||||
defaultMessage="Looking for matching indices…"
|
||||
/>
|
||||
</h3>
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { CreateButton } from './create_button';
|
||||
export { PreviewPanel } from './preview_panel';
|
|
@ -47,7 +47,7 @@ exports[`IndicesList should change pages 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Rows per page: {perPage}"
|
||||
id="indexPatternManagement.createIndexPattern.step.pagingLabel"
|
||||
id="indexPatternEditor.pagingLabel"
|
||||
values={
|
||||
Object {
|
||||
"perPage": 1,
|
||||
|
@ -140,7 +140,7 @@ exports[`IndicesList should change per page 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Rows per page: {perPage}"
|
||||
id="indexPatternManagement.createIndexPattern.step.pagingLabel"
|
||||
id="indexPatternEditor.pagingLabel"
|
||||
values={
|
||||
Object {
|
||||
"perPage": 1,
|
||||
|
@ -255,7 +255,7 @@ exports[`IndicesList should highlight the query in the matches 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Rows per page: {perPage}"
|
||||
id="indexPatternManagement.createIndexPattern.step.pagingLabel"
|
||||
id="indexPatternEditor.pagingLabel"
|
||||
values={
|
||||
Object {
|
||||
"perPage": 10,
|
||||
|
@ -356,7 +356,7 @@ exports[`IndicesList should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Rows per page: {perPage}"
|
||||
id="indexPatternManagement.createIndexPattern.step.pagingLabel"
|
||||
id="indexPatternEditor.pagingLabel"
|
||||
values={
|
||||
Object {
|
||||
"perPage": 10,
|
||||
|
@ -521,7 +521,7 @@ exports[`IndicesList updating props should render all new indices 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Rows per page: {perPage}"
|
||||
id="indexPatternManagement.createIndexPattern.step.pagingLabel"
|
||||
id="indexPatternEditor.pagingLabel"
|
||||
values={
|
||||
Object {
|
||||
"perPage": 10,
|
|
@ -9,7 +9,7 @@
|
|||
import React from 'react';
|
||||
import { IndicesList } from '../indices_list';
|
||||
import { shallow } from 'enzyme';
|
||||
import { MatchedItem } from '../../../../types';
|
||||
import { MatchedItem } from '../../../types';
|
||||
|
||||
const indices = ([
|
||||
{ name: 'kibana', tags: [] },
|
|
@ -27,8 +27,7 @@ import {
|
|||
import { Pager } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { PER_PAGE_INCREMENTS } from '../../../../constants';
|
||||
import { MatchedItem, Tag } from '../../../../types';
|
||||
import { MatchedItem, Tag } from '../../../types';
|
||||
|
||||
interface IndicesListProps {
|
||||
indices: MatchedItem[];
|
||||
|
@ -41,6 +40,8 @@ interface IndicesListState {
|
|||
isPerPageControlOpen: boolean;
|
||||
}
|
||||
|
||||
const PER_PAGE_INCREMENTS = [5, 10, 20, 50];
|
||||
|
||||
export class IndicesList extends React.Component<IndicesListProps, IndicesListState> {
|
||||
pager: Pager;
|
||||
constructor(props: IndicesListProps) {
|
||||
|
@ -96,7 +97,7 @@ export class IndicesList extends React.Component<IndicesListProps, IndicesListSt
|
|||
onClick={this.openPerPageControl}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.pagingLabel"
|
||||
id="indexPatternEditor.pagingLabel"
|
||||
defaultMessage="Rows per page: {perPage}"
|
||||
values={{ perPage }}
|
||||
/>
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { StatusMessage } from './status_message';
|
||||
import { IndicesList } from './indices_list';
|
||||
|
||||
import { INDEX_PATTERN_TYPE, MatchedIndicesSet } from '../../types';
|
||||
|
||||
interface Props {
|
||||
type: INDEX_PATTERN_TYPE;
|
||||
allowHidden: boolean;
|
||||
title: string;
|
||||
matched: MatchedIndicesSet;
|
||||
}
|
||||
|
||||
export const PreviewPanel = ({ type, allowHidden, title = '', matched }: Props) => {
|
||||
const indicesListContent =
|
||||
matched.visibleIndices.length || matched.allIndices.length ? (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<IndicesList
|
||||
data-test-subj="createIndexPatternStep1IndicesList"
|
||||
query={title}
|
||||
indices={title.length ? matched.visibleIndices : matched.allIndices}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusMessage
|
||||
matchedIndices={matched}
|
||||
showSystemIndices={type === INDEX_PATTERN_TYPE.ROLLUP ? false : true}
|
||||
isIncludingSystemIndices={allowHidden}
|
||||
query={title}
|
||||
/>
|
||||
{indicesListContent}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -11,7 +11,7 @@ exports[`StatusMessage should render with exact matches 1`] = `
|
|||
|
||||
<FormattedMessage
|
||||
defaultMessage="Your index pattern matches {sourceCount} {sourceCount, plural, one {source} other {sources} }."
|
||||
id="indexPatternManagement.createIndexPattern.step.status.successLabel.successDetail"
|
||||
id="indexPatternEditor.status.successLabel.successDetail"
|
||||
values={
|
||||
Object {
|
||||
"sourceCount": 1,
|
||||
|
@ -31,15 +31,14 @@ exports[`StatusMessage should render with no partial matches 1`] = `
|
|||
title={
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="The index pattern you've entered doesn't match any indices. You can match {indicesLength, plural, one {your} other {any of your} } {strongIndices}, below."
|
||||
id="indexPatternManagement.createIndexPattern.step.status.notMatchLabel.notMatchDetail"
|
||||
defaultMessage="The index pattern you've entered doesn't match any indices, data streams, or index aliases. You can match {strongIndices}, below."
|
||||
id="indexPatternEditor.status.notMatchLabel.notMatchDetail"
|
||||
values={
|
||||
Object {
|
||||
"indicesLength": 2,
|
||||
"strongIndices": <strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="{indicesLength, plural, one {# index} other {# indices} }"
|
||||
id="indexPatternManagement.createIndexPattern.step.status.notMatchLabel.allIndicesLabel"
|
||||
defaultMessage="{indicesLength, plural, one {# source} other {# sources} }"
|
||||
id="indexPatternEditor.status.notMatchLabel.allIndicesLabel"
|
||||
values={
|
||||
Object {
|
||||
"indicesLength": 2,
|
||||
|
@ -63,15 +62,15 @@ exports[`StatusMessage should render with partial matches 1`] = `
|
|||
title={
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your index pattern doesn't match any indices, but you have {strongIndices} which {matchedIndicesLength, plural, one {looks} other {look} } similar."
|
||||
id="indexPatternManagement.createIndexPattern.step.status.partialMatchLabel.partialMatchDetail"
|
||||
defaultMessage="Your index pattern doesn't match any sources, but you have {strongIndices} which {matchedIndicesLength, plural, one {is} other {are} } similar."
|
||||
id="indexPatternEditor.status.partialMatchLabel.partialMatchDetail"
|
||||
values={
|
||||
Object {
|
||||
"matchedIndicesLength": 1,
|
||||
"strongIndices": <strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="{matchedIndicesLength, plural, one {index} other {# indices} }"
|
||||
id="indexPatternManagement.createIndexPattern.step.status.partialMatchLabel.strongIndicesLabel"
|
||||
defaultMessage="{matchedIndicesLength, plural, one {source} other {# sources} }"
|
||||
id="indexPatternEditor.status.partialMatchLabel.strongIndicesLabel"
|
||||
values={
|
||||
Object {
|
||||
"matchedIndicesLength": 1,
|
||||
|
@ -95,8 +94,8 @@ exports[`StatusMessage should render without a query 1`] = `
|
|||
title={
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your index pattern can match {sourceCount, plural, one {your # source} other {any of your # sources} }."
|
||||
id="indexPatternManagement.createIndexPattern.step.status.matchAnyLabel.matchAnyDetail"
|
||||
defaultMessage="Your index pattern has {sourceCount, plural, one {# match} other {# matches} }."
|
||||
id="indexPatternEditor.status.matchAnyLabel.matchAnyDetail"
|
||||
values={
|
||||
Object {
|
||||
"sourceCount": 2,
|
||||
|
@ -117,7 +116,7 @@ exports[`StatusMessage should show that no indices exist 1`] = `
|
|||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="No Elasticsearch indices match your pattern."
|
||||
id="indexPatternManagement.createIndexPattern.step.status.noSystemIndicesLabel"
|
||||
id="indexPatternEditor.status.noSystemIndicesLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</span>
|
||||
|
@ -134,7 +133,7 @@ exports[`StatusMessage should show that system indices exist 1`] = `
|
|||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="No Elasticsearch indices match your pattern."
|
||||
id="indexPatternManagement.createIndexPattern.step.status.noSystemIndicesLabel"
|
||||
id="indexPatternEditor.status.noSystemIndicesLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</span>
|
|
@ -9,7 +9,7 @@
|
|||
import React from 'react';
|
||||
import { StatusMessage } from '../status_message';
|
||||
import { shallow } from 'enzyme';
|
||||
import { MatchedItem } from '../../../../types';
|
||||
import { MatchedItem } from '../../../types';
|
||||
|
||||
const tagsPartial = {
|
||||
tags: [],
|
||||
|
@ -22,6 +22,7 @@ const matchedIndices = {
|
|||
] as unknown) as MatchedItem[],
|
||||
exactMatchedIndices: [] as MatchedItem[],
|
||||
partialMatchedIndices: ([{ name: 'kibana', ...tagsPartial }] as unknown) as MatchedItem[],
|
||||
visibleIndices: [],
|
||||
};
|
||||
|
||||
describe('StatusMessage', () => {
|
||||
|
@ -94,6 +95,7 @@ describe('StatusMessage', () => {
|
|||
allIndices: [],
|
||||
exactMatchedIndices: [],
|
||||
partialMatchedIndices: [],
|
||||
visibleIndices: [],
|
||||
}}
|
||||
isIncludingSystemIndices={false}
|
||||
query={''}
|
||||
|
@ -111,6 +113,7 @@ describe('StatusMessage', () => {
|
|||
allIndices: [],
|
||||
exactMatchedIndices: [],
|
||||
partialMatchedIndices: [],
|
||||
visibleIndices: [],
|
||||
}}
|
||||
isIncludingSystemIndices={true}
|
||||
query={''}
|
|
@ -12,19 +12,48 @@ import { EuiCallOut } from '@elastic/eui';
|
|||
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { MatchedItem } from '../../../../types';
|
||||
import { MatchedIndicesSet } from '../../../types';
|
||||
|
||||
interface StatusMessageProps {
|
||||
matchedIndices: {
|
||||
allIndices: MatchedItem[];
|
||||
exactMatchedIndices: MatchedItem[];
|
||||
partialMatchedIndices: MatchedItem[];
|
||||
};
|
||||
matchedIndices: MatchedIndicesSet;
|
||||
isIncludingSystemIndices: boolean;
|
||||
query: string;
|
||||
showSystemIndices: boolean;
|
||||
}
|
||||
|
||||
const NoMatchStatusMessage = (allIndicesLength: number) => (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="indexPatternEditor.status.notMatchLabel.notMatchDetail"
|
||||
defaultMessage="The index pattern you've entered doesn't match any indices, data streams, or index aliases.
|
||||
You can match {strongIndices}, below."
|
||||
values={{
|
||||
strongIndices: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="indexPatternEditor.status.notMatchLabel.allIndicesLabel"
|
||||
defaultMessage="{indicesLength, plural,
|
||||
one {# source}
|
||||
other {# sources}
|
||||
}"
|
||||
values={{ indicesLength: allIndicesLength }}
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
const NoMatchNoIndicesStatusMessage = () => (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="indexPatternEditor.status.notMatchLabel.notMatchNoIndicesDetail"
|
||||
defaultMessage="The index pattern you've entered doesn't match any indices."
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
export const StatusMessage: React.FC<StatusMessageProps> = ({
|
||||
matchedIndices: { allIndices = [], exactMatchedIndices = [], partialMatchedIndices = [] },
|
||||
isIncludingSystemIndices,
|
||||
|
@ -45,10 +74,10 @@ export const StatusMessage: React.FC<StatusMessageProps> = ({
|
|||
statusMessage = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.status.matchAnyLabel.matchAnyDetail"
|
||||
defaultMessage="Your index pattern can match {sourceCount, plural,
|
||||
one {your # source}
|
||||
other {any of your # sources}
|
||||
id="indexPatternEditor.status.matchAnyLabel.matchAnyDetail"
|
||||
defaultMessage="Your index pattern has {sourceCount, plural,
|
||||
one {# match}
|
||||
other {# matches}
|
||||
}."
|
||||
values={{ sourceCount: allIndicesLength }}
|
||||
/>
|
||||
|
@ -58,8 +87,8 @@ export const StatusMessage: React.FC<StatusMessageProps> = ({
|
|||
statusMessage = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.status.noSystemIndicesWithPromptLabel"
|
||||
defaultMessage="No Elasticsearch indices match your pattern. To view the matching system indices, toggle the switch above."
|
||||
id="indexPatternEditor.status.noSystemIndicesWithPromptLabel"
|
||||
defaultMessage="No Elasticsearch indices match your pattern."
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
@ -67,7 +96,7 @@ export const StatusMessage: React.FC<StatusMessageProps> = ({
|
|||
statusMessage = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.status.noSystemIndicesLabel"
|
||||
id="indexPatternEditor.status.noSystemIndicesLabel"
|
||||
defaultMessage="No Elasticsearch indices match your pattern."
|
||||
/>
|
||||
</span>
|
||||
|
@ -80,7 +109,7 @@ export const StatusMessage: React.FC<StatusMessageProps> = ({
|
|||
<span>
|
||||
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.status.successLabel.successDetail"
|
||||
id="indexPatternEditor.status.successLabel.successDetail"
|
||||
defaultMessage="Your index pattern matches {sourceCount} {sourceCount, plural,
|
||||
one {source}
|
||||
other {sources}
|
||||
|
@ -97,21 +126,21 @@ export const StatusMessage: React.FC<StatusMessageProps> = ({
|
|||
statusMessage = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.status.partialMatchLabel.partialMatchDetail"
|
||||
defaultMessage="Your index pattern doesn't match any indices, but you have {strongIndices} which
|
||||
id="indexPatternEditor.status.partialMatchLabel.partialMatchDetail"
|
||||
defaultMessage="Your index pattern doesn't match any sources, but you have {strongIndices} which
|
||||
{matchedIndicesLength, plural,
|
||||
one {looks}
|
||||
other {look}
|
||||
one {is}
|
||||
other {are}
|
||||
} similar."
|
||||
values={{
|
||||
matchedIndicesLength: partialMatchedIndices.length,
|
||||
strongIndices: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.status.partialMatchLabel.strongIndicesLabel"
|
||||
id="indexPatternEditor.status.partialMatchLabel.strongIndicesLabel"
|
||||
defaultMessage="{matchedIndicesLength, plural,
|
||||
one {index}
|
||||
other {# indices}
|
||||
one {source}
|
||||
other {# sources}
|
||||
}"
|
||||
values={{ matchedIndicesLength: partialMatchedIndices.length }}
|
||||
/>
|
||||
|
@ -124,33 +153,9 @@ export const StatusMessage: React.FC<StatusMessageProps> = ({
|
|||
} else {
|
||||
statusIcon = undefined;
|
||||
statusColor = 'warning';
|
||||
statusMessage = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.status.notMatchLabel.notMatchDetail"
|
||||
defaultMessage="The index pattern you've entered doesn't match any indices.
|
||||
You can match {indicesLength, plural,
|
||||
one {your}
|
||||
other {any of your}
|
||||
} {strongIndices}, below."
|
||||
values={{
|
||||
strongIndices: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.createIndexPattern.step.status.notMatchLabel.allIndicesLabel"
|
||||
defaultMessage="{indicesLength, plural,
|
||||
one {# index}
|
||||
other {# indices}
|
||||
}"
|
||||
values={{ indicesLength: allIndicesLength }}
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
indicesLength: allIndicesLength,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
statusMessage = allIndicesLength
|
||||
? NoMatchStatusMessage(allIndicesLength)
|
||||
: NoMatchNoIndicesStatusMessage();
|
||||
}
|
||||
|
||||
return (
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { RollupBetaWarning } from './rollup_beta_warning';
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
const rollupBetaWarningTitle = i18n.translate(
|
||||
'indexPatternEditor.rollupIndexPattern.warning.title',
|
||||
{
|
||||
defaultMessage: 'Beta feature',
|
||||
}
|
||||
);
|
||||
|
||||
export const RollupBetaWarning = () => (
|
||||
<EuiCallOut title={rollupBetaWarningTitle} color="warning" iconType="help">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="indexPatternEditor.rollupIndexPattern.warning.textParagraphOne"
|
||||
defaultMessage="Kibana's support for rollup index patterns is in beta. You might encounter
|
||||
issues using these patterns in saved searches, visualizations, and dashboards. They
|
||||
are not supported in some advanced features, such as Timelion, and Machine Learning."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="indexPatternEditor.rollupIndexPattern.warning.textParagraphTwo"
|
||||
defaultMessage="You can match a rollup index pattern against one rollup index and zero or more
|
||||
regular indices. A rollup index pattern has limited metrics, fields, intervals, and
|
||||
aggregations. A rollup index is limited to indices that have one job configuration,
|
||||
or multiple jobs with compatible configurations."
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
11
src/plugins/index_pattern_editor/public/constants.ts
Normal file
11
src/plugins/index_pattern_editor/public/constants.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const pluginName = 'index_pattern_editor';
|
||||
export const MAX_NUMBER_OF_MATCHING_INDICES = 100;
|
||||
export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns';
|
27
src/plugins/index_pattern_editor/public/index.ts
Normal file
27
src/plugins/index_pattern_editor/public/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Management Plugin - public
|
||||
*
|
||||
* This is the entry point for the entire client-side public contract of the plugin.
|
||||
* If something is not explicitly exported here, you can safely assume it is private
|
||||
* to the plugin and not considered stable.
|
||||
*
|
||||
* All stateful contracts will be injected by the platform at runtime, and are defined
|
||||
* in the setup/start interfaces in `plugin.ts`. The remaining items exported here are
|
||||
* either types, or static code.
|
||||
*/
|
||||
|
||||
import { IndexPatternEditorPlugin } from './plugin';
|
||||
|
||||
export type { PluginStart as IndexPatternEditorStart, IndexPatternEditorProps } from './types';
|
||||
|
||||
export function plugin() {
|
||||
return new IndexPatternEditorPlugin();
|
||||
}
|
|
@ -9,28 +9,25 @@
|
|||
import { ensureMinimumTime } from './ensure_minimum_time';
|
||||
|
||||
describe('ensureMinimumTime', () => {
|
||||
it('resolves single promise', async (done) => {
|
||||
it('resolves single promise', async () => {
|
||||
const promiseA = new Promise((resolve) => resolve('a'));
|
||||
const a = await ensureMinimumTime(promiseA, 0);
|
||||
expect(a).toBe('a');
|
||||
done();
|
||||
});
|
||||
|
||||
it('resolves multiple promises', async (done) => {
|
||||
const promiseA = new Promise((resolve) => resolve('a'));
|
||||
const promiseB = new Promise((resolve) => resolve('b'));
|
||||
it('resolves multiple promises', async () => {
|
||||
const promiseA = new Promise<string>((resolve) => resolve('a'));
|
||||
const promiseB = new Promise<string>((resolve) => resolve('b'));
|
||||
const [a, b] = await ensureMinimumTime([promiseA, promiseB], 0);
|
||||
expect(a).toBe('a');
|
||||
expect(b).toBe('b');
|
||||
done();
|
||||
});
|
||||
|
||||
it('resolves in the amount of time provided, at minimum', async (done) => {
|
||||
it('resolves in the amount of time provided, at minimum', async () => {
|
||||
const startTime = new Date().getTime();
|
||||
const promise = new Promise<void>((resolve) => resolve());
|
||||
await ensureMinimumTime(promise, 100);
|
||||
const endTime = new Date().getTime();
|
||||
expect(endTime - startTime).toBeGreaterThanOrEqual(100);
|
||||
done();
|
||||
});
|
||||
});
|
|
@ -15,8 +15,8 @@
|
|||
|
||||
export const DEFAULT_MINIMUM_TIME_MS = 300;
|
||||
|
||||
export async function ensureMinimumTime(
|
||||
promiseOrPromises: Promise<any> | Array<Promise<any>>,
|
||||
export async function ensureMinimumTime<T>(
|
||||
promiseOrPromises: Promise<T> | Array<Promise<T>>,
|
||||
minimumTimeMs = DEFAULT_MINIMUM_TIME_MS
|
||||
) {
|
||||
let returnValue;
|
|
@ -16,18 +16,16 @@ describe('extractTimeFields', () => {
|
|||
{ type: 'text', name: 'name' },
|
||||
] as IndexPatternField[];
|
||||
|
||||
expect(extractTimeFields(fields)).toEqual([
|
||||
{ display: `The indices which match this index pattern don't contain any time fields.` },
|
||||
]);
|
||||
expect(extractTimeFields(fields)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should add extra options', () => {
|
||||
const fields = [{ type: 'date', name: '@timestamp' }] as IndexPatternField[];
|
||||
|
||||
// const extractedFields = extractTimeFields(fields);
|
||||
expect(extractTimeFields(fields)).toEqual([
|
||||
{ display: '@timestamp', fieldName: '@timestamp' },
|
||||
{ isDisabled: true, display: '───', fieldName: '' },
|
||||
{ display: `I don't want to use the time filter`, fieldName: undefined },
|
||||
{ display: `--- I don't want to use the time filter ---`, fieldName: '' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IndexPatternField } from '../../../../plugins/data/public';
|
||||
import { TimestampOption } from '../types';
|
||||
|
||||
export function extractTimeFields(
|
||||
fields: IndexPatternField[],
|
||||
requireTimestampField: boolean = false
|
||||
): TimestampOption[] {
|
||||
const dateFields = fields.filter((field) => field.type === 'date');
|
||||
|
||||
if (dateFields.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const noTimeFieldLabel = i18n.translate(
|
||||
'indexPatternEditor.createIndexPattern.stepTime.noTimeFieldOptionLabel',
|
||||
{
|
||||
defaultMessage: "--- I don't want to use the time filter ---",
|
||||
}
|
||||
);
|
||||
const noTimeFieldOption = {
|
||||
display: noTimeFieldLabel,
|
||||
fieldName: '',
|
||||
};
|
||||
|
||||
const timeFields = dateFields.map((field) => ({
|
||||
display: field.name,
|
||||
fieldName: field.name,
|
||||
}));
|
||||
|
||||
if (!requireTimestampField) {
|
||||
timeFields.push(noTimeFieldOption);
|
||||
}
|
||||
|
||||
return timeFields;
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { getIndices, responseToItemArray } from './get_indices';
|
||||
import { httpServiceMock } from '../../../../../../core/public/mocks';
|
||||
import { httpServiceMock } from '../../../../core/public/mocks';
|
||||
import { ResolveIndexResponseItemIndexAttrs } from '../types';
|
||||
|
||||
export const successfulResponse = {
|
||||
|
@ -33,26 +33,27 @@ export const successfulResponse = {
|
|||
};
|
||||
|
||||
const mockGetTags = () => [];
|
||||
const mockIsRollupIndex = () => false;
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
http.get.mockResolvedValue(successfulResponse);
|
||||
|
||||
describe('getIndices', () => {
|
||||
it('should work in a basic case', async () => {
|
||||
const result = await getIndices(http, mockGetTags, 'kibana', false);
|
||||
const result = await getIndices(http, mockIsRollupIndex, 'kibana', false);
|
||||
expect(result.length).toBe(3);
|
||||
expect(result[0].name).toBe('f-alias');
|
||||
expect(result[1].name).toBe('foo');
|
||||
});
|
||||
|
||||
it('should ignore ccs query-all', async () => {
|
||||
expect((await getIndices(http, mockGetTags, '*:', false)).length).toBe(0);
|
||||
expect((await getIndices(http, mockIsRollupIndex, '*:', false)).length).toBe(0);
|
||||
});
|
||||
|
||||
it('should ignore a single comma', async () => {
|
||||
expect((await getIndices(http, mockGetTags, ',', false)).length).toBe(0);
|
||||
expect((await getIndices(http, mockGetTags, ',*', false)).length).toBe(0);
|
||||
expect((await getIndices(http, mockGetTags, ',foobar', false)).length).toBe(0);
|
||||
expect((await getIndices(http, mockIsRollupIndex, ',', false)).length).toBe(0);
|
||||
expect((await getIndices(http, mockIsRollupIndex, ',*', false)).length).toBe(0);
|
||||
expect((await getIndices(http, mockIsRollupIndex, ',foobar', false)).length).toBe(0);
|
||||
});
|
||||
|
||||
it('response object to item array', () => {
|
||||
|
@ -89,7 +90,7 @@ describe('getIndices', () => {
|
|||
http.get.mockImplementationOnce(() => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
const result = await getIndices(http, mockGetTags, 'kibana', false);
|
||||
const result = await getIndices(http, mockIsRollupIndex, 'kibana', false);
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
});
|
|
@ -9,25 +9,41 @@
|
|||
import { sortBy } from 'lodash';
|
||||
import { HttpStart } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IndexPatternCreationConfig } from '../../../../../index_pattern_management/public';
|
||||
import { Tag, INDEX_PATTERN_TYPE } from '../types';
|
||||
// todo move into this plugin, consider removing all ipm references
|
||||
import { MatchedItem, ResolveIndexResponse, ResolveIndexResponseItemIndexAttrs } from '../types';
|
||||
|
||||
const aliasLabel = i18n.translate('indexPatternManagement.aliasLabel', { defaultMessage: 'Alias' });
|
||||
const dataStreamLabel = i18n.translate('indexPatternManagement.dataStreamLabel', {
|
||||
const aliasLabel = i18n.translate('indexPatternEditor.aliasLabel', { defaultMessage: 'Alias' });
|
||||
const dataStreamLabel = i18n.translate('indexPatternEditor.dataStreamLabel', {
|
||||
defaultMessage: 'Data stream',
|
||||
});
|
||||
|
||||
const indexLabel = i18n.translate('indexPatternManagement.indexLabel', {
|
||||
const indexLabel = i18n.translate('indexPatternEditor.indexLabel', {
|
||||
defaultMessage: 'Index',
|
||||
});
|
||||
|
||||
const frozenLabel = i18n.translate('indexPatternManagement.frozenLabel', {
|
||||
const frozenLabel = i18n.translate('indexPatternEditor.frozenLabel', {
|
||||
defaultMessage: 'Frozen',
|
||||
});
|
||||
|
||||
const rollupLabel = i18n.translate('indexPatternEditor.rollupLabel', {
|
||||
defaultMessage: 'Rollup',
|
||||
});
|
||||
|
||||
const getIndexTags = (isRollupIndex: (indexName: string) => boolean) => (indexName: string) =>
|
||||
isRollupIndex(indexName)
|
||||
? [
|
||||
{
|
||||
key: INDEX_PATTERN_TYPE.ROLLUP,
|
||||
name: rollupLabel,
|
||||
color: 'primary',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
export async function getIndices(
|
||||
http: HttpStart,
|
||||
getIndexTags: IndexPatternCreationConfig['getIndexTags'],
|
||||
isRollupIndex: (indexName: string) => boolean,
|
||||
rawPattern: string,
|
||||
showAllIndices: boolean
|
||||
): Promise<MatchedItem[]> {
|
||||
|
@ -62,7 +78,7 @@ export async function getIndices(
|
|||
return [];
|
||||
}
|
||||
|
||||
return responseToItemArray(response, getIndexTags);
|
||||
return responseToItemArray(response, getIndexTags(isRollupIndex));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
@ -70,7 +86,7 @@ export async function getIndices(
|
|||
|
||||
export const responseToItemArray = (
|
||||
response: ResolveIndexResponse,
|
||||
getIndexTags: IndexPatternCreationConfig['getIndexTags']
|
||||
getTags: (indexName: string) => Tag[]
|
||||
): MatchedItem[] => {
|
||||
const source: MatchedItem[] = [];
|
||||
|
||||
|
@ -78,7 +94,7 @@ export const responseToItemArray = (
|
|||
const tags: MatchedItem['tags'] = [{ key: 'index', name: indexLabel, color: 'default' }];
|
||||
const isFrozen = (index.attributes || []).includes(ResolveIndexResponseItemIndexAttrs.FROZEN);
|
||||
|
||||
tags.push(...getIndexTags(index.name));
|
||||
tags.push(...getTags(index.name));
|
||||
if (isFrozen) {
|
||||
tags.push({ name: frozenLabel, key: 'frozen', color: 'danger' });
|
||||
}
|
|
@ -23,10 +23,6 @@ function isSystemIndex(index: string): boolean {
|
|||
}
|
||||
|
||||
function filterSystemIndices(indices: MatchedItem[], isIncludingSystemIndices: boolean) {
|
||||
if (!indices) {
|
||||
return indices;
|
||||
}
|
||||
|
||||
const acceptableIndices = isIncludingSystemIndices
|
||||
? indices
|
||||
: // All system indices begin with a period.
|
||||
|
@ -54,14 +50,14 @@ function filterSystemIndices(indices: MatchedItem[], isIncludingSystemIndices: b
|
|||
We call this `exact` matches because ES is telling us exactly what it matches
|
||||
*/
|
||||
|
||||
import { MatchedItem } from '../types';
|
||||
import { MatchedItem, MatchedIndicesSet } from '../types';
|
||||
|
||||
export function getMatchedIndices(
|
||||
unfilteredAllIndices: MatchedItem[],
|
||||
unfilteredPartialMatchedIndices: MatchedItem[],
|
||||
unfilteredExactMatchedIndices: MatchedItem[],
|
||||
isIncludingSystemIndices: boolean = false
|
||||
) {
|
||||
): MatchedIndicesSet {
|
||||
const allIndices = filterSystemIndices(unfilteredAllIndices, isIncludingSystemIndices);
|
||||
const partialMatchedIndices = filterSystemIndices(
|
||||
unfilteredPartialMatchedIndices,
|
32
src/plugins/index_pattern_editor/public/mocks.ts
Normal file
32
src/plugins/index_pattern_editor/public/mocks.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { IndexPatternEditorPlugin } from './plugin';
|
||||
|
||||
export type Start = jest.Mocked<ReturnType<IndexPatternEditorPlugin['start']>>;
|
||||
|
||||
export type Setup = jest.Mocked<ReturnType<IndexPatternEditorPlugin['setup']>>;
|
||||
|
||||
const createSetupContract = (): Setup => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const createStartContract = (): Start => {
|
||||
return {
|
||||
openEditor: jest.fn(),
|
||||
IndexPatternEditorComponent: jest.fn(),
|
||||
userPermissions: {
|
||||
editIndexPattern: jest.fn(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const indexPatternEditorPluginMock = {
|
||||
createSetupContract,
|
||||
createStartContract,
|
||||
};
|
92
src/plugins/index_pattern_editor/public/open_editor.tsx
Normal file
92
src/plugins/index_pattern_editor/public/open_editor.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CoreStart, OverlayRef } from 'src/core/public';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
createKibanaReactContext,
|
||||
toMountPoint,
|
||||
IndexPattern,
|
||||
DataPublicPluginStart,
|
||||
} from './shared_imports';
|
||||
|
||||
import { CloseEditor, IndexPatternEditorContext, IndexPatternEditorProps } from './types';
|
||||
import { IndexPatternEditorLazy } from './components/index_pattern_editor_lazy';
|
||||
|
||||
interface Dependencies {
|
||||
core: CoreStart;
|
||||
indexPatternService: DataPublicPluginStart['indexPatterns'];
|
||||
}
|
||||
|
||||
export const getEditorOpener = ({ core, indexPatternService }: Dependencies) => (
|
||||
options: IndexPatternEditorProps
|
||||
): CloseEditor => {
|
||||
const { uiSettings, overlays, docLinks, notifications, http, application } = core;
|
||||
const {
|
||||
Provider: KibanaReactContextProvider,
|
||||
} = createKibanaReactContext<IndexPatternEditorContext>({
|
||||
uiSettings,
|
||||
docLinks,
|
||||
http,
|
||||
notifications,
|
||||
application,
|
||||
indexPatternService,
|
||||
});
|
||||
|
||||
let overlayRef: OverlayRef | null = null;
|
||||
|
||||
const openEditor = ({
|
||||
onSave,
|
||||
onCancel = () => {},
|
||||
defaultTypeIsRollup = false,
|
||||
requireTimestampField = false,
|
||||
}: IndexPatternEditorProps): CloseEditor => {
|
||||
const closeEditor = () => {
|
||||
if (overlayRef) {
|
||||
overlayRef.close();
|
||||
overlayRef = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveIndexPattern = (indexPattern: IndexPattern) => {
|
||||
closeEditor();
|
||||
|
||||
if (onSave) {
|
||||
onSave(indexPattern);
|
||||
}
|
||||
};
|
||||
|
||||
overlayRef = overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<KibanaReactContextProvider>
|
||||
<I18nProvider>
|
||||
<IndexPatternEditorLazy
|
||||
onSave={onSaveIndexPattern}
|
||||
onCancel={() => {
|
||||
closeEditor();
|
||||
onCancel();
|
||||
}}
|
||||
defaultTypeIsRollup={defaultTypeIsRollup}
|
||||
requireTimestampField={requireTimestampField}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</KibanaReactContextProvider>
|
||||
),
|
||||
{
|
||||
hideCloseButton: true,
|
||||
size: 'l',
|
||||
}
|
||||
);
|
||||
|
||||
return closeEditor;
|
||||
};
|
||||
|
||||
return openEditor(options);
|
||||
};
|
83
src/plugins/index_pattern_editor/public/plugin.test.tsx
Normal file
83
src/plugins/index_pattern_editor/public/plugin.test.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
jest.mock('../../kibana_react/public', () => {
|
||||
const original = jest.requireActual('../../kibana_react/public');
|
||||
|
||||
return {
|
||||
...original,
|
||||
toMountPoint: (node: React.ReactNode) => node,
|
||||
};
|
||||
});
|
||||
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { dataPluginMock } from '../../data/public/mocks';
|
||||
import { usageCollectionPluginMock } from '../../usage_collection/public/mocks';
|
||||
|
||||
import { IndexPatternEditorLazy } from './components/index_pattern_editor_lazy';
|
||||
import { IndexPatternEditorPlugin } from './plugin';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
describe('IndexPatternEditorPlugin', () => {
|
||||
const coreStart: CoreStart = coreMock.createStart();
|
||||
const pluginStart = {
|
||||
data: dataPluginMock.createStartContract(),
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
};
|
||||
|
||||
let plugin: IndexPatternEditorPlugin;
|
||||
|
||||
beforeEach(() => {
|
||||
plugin = new IndexPatternEditorPlugin();
|
||||
});
|
||||
|
||||
test('should expose a handler to open the indexpattern field editor', async () => {
|
||||
const startApi = await plugin.start(coreStart, pluginStart);
|
||||
expect(startApi.openEditor).toBeDefined();
|
||||
});
|
||||
|
||||
test('should call core.overlays.openFlyout when opening the editor', async () => {
|
||||
const openFlyout = jest.fn();
|
||||
const onSaveSpy = jest.fn();
|
||||
|
||||
const coreStartMocked = {
|
||||
...coreStart,
|
||||
overlays: {
|
||||
...coreStart.overlays,
|
||||
openFlyout,
|
||||
},
|
||||
};
|
||||
const { openEditor } = await plugin.start(coreStartMocked, pluginStart);
|
||||
|
||||
openEditor({ onSave: onSaveSpy });
|
||||
|
||||
expect(openFlyout).toHaveBeenCalled();
|
||||
|
||||
const [[arg]] = openFlyout.mock.calls;
|
||||
const i18nProvider = arg.props.children;
|
||||
expect(i18nProvider.props.children.type).toBe(IndexPatternEditorLazy);
|
||||
|
||||
// We force call the "onSave" prop from the <RuntimeFieldEditorFlyoutContent /> component
|
||||
// and make sure that the the spy is being called.
|
||||
// Note: we are testing implementation details, if we change or rename the "onSave" prop on
|
||||
// the component, we will need to update this test accordingly.
|
||||
expect(i18nProvider.props.children.props.onSave).toBeDefined();
|
||||
i18nProvider.props.children.props.onSave();
|
||||
expect(onSaveSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should return a handler to close the flyout', async () => {
|
||||
const { openEditor } = await plugin.start(coreStart, pluginStart);
|
||||
|
||||
const closeEditorHandler = openEditor({ onSave: noop });
|
||||
expect(typeof closeEditorHandler).toBe('function');
|
||||
});
|
||||
});
|
76
src/plugins/index_pattern_editor/public/plugin.tsx
Normal file
76
src/plugins/index_pattern_editor/public/plugin.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Plugin, CoreSetup, CoreStart } from 'src/core/public';
|
||||
|
||||
import {
|
||||
PluginSetup,
|
||||
PluginStart,
|
||||
SetupPlugins,
|
||||
StartPlugins,
|
||||
IndexPatternEditorProps,
|
||||
} from './types';
|
||||
import { getEditorOpener } from './open_editor';
|
||||
import { IndexPatternEditor } from './components/index_pattern_editor';
|
||||
|
||||
export class IndexPatternEditorPlugin
|
||||
implements Plugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {
|
||||
public setup(core: CoreSetup<StartPlugins, PluginStart>, plugins: SetupPlugins): PluginSetup {
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: StartPlugins) {
|
||||
const { application, uiSettings, docLinks, http, notifications } = core;
|
||||
const { data } = plugins;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Index pattern editor flyout via function interface
|
||||
* @param IndexPatternEditorProps - index pattern editor config
|
||||
* @returns method to close editor
|
||||
*/
|
||||
openEditor: getEditorOpener({
|
||||
core,
|
||||
indexPatternService: data.indexPatterns,
|
||||
}),
|
||||
/**
|
||||
* Index pattern editor flyout via react component
|
||||
* @param IndexPatternEditorProps - index pattern editor config
|
||||
* @returns JSX.Element
|
||||
*/
|
||||
IndexPatternEditorComponent: (props: IndexPatternEditorProps) => (
|
||||
<IndexPatternEditor
|
||||
services={{
|
||||
uiSettings,
|
||||
docLinks,
|
||||
http,
|
||||
notifications,
|
||||
application,
|
||||
indexPatternService: data.indexPatterns,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
/**
|
||||
* Convenience method to determine whether the user can create or edit edit the index patterns.
|
||||
*
|
||||
* @returns boolean
|
||||
*/
|
||||
userPermissions: {
|
||||
editIndexPattern: () => {
|
||||
return application.capabilities.management.kibana.indexPatterns;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {
|
||||
return {};
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue