[index patterns] index pattern create modal (#101853)

index pattern creation flyout
This commit is contained in:
Matthew Kime 2021-08-05 22:58:57 -05:00 committed by GitHub
parent 923eca0adf
commit d44df74598
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
185 changed files with 3400 additions and 5834 deletions

View file

@ -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",

View file

@ -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!

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) &gt; [hideCloseButton](./kibana-plugin-core-public.overlayflyoutopenoptions.hideclosebutton.md)
## OverlayFlyoutOpenOptions.hideCloseButton property
<b>Signature:</b>
```typescript
hideCloseButton?: boolean;
```

View file

@ -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 &#124; number &#124; string</code> | |
| [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | <code>boolean</code> | |
| [size](./kibana-plugin-core-public.overlayflyoutopenoptions.size.md) | <code>EuiFlyoutSize</code> | |

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IndexPatternAggRestrictions](./kibana-plugin-plugins-data-public.indexpatternaggrestrictions.md)
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggregationRestrictions](./kibana-plugin-plugins-data-public.aggregationrestrictions.md)
## IndexPatternAggRestrictions type
## AggregationRestrictions type
<b>Signature:</b>

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) &gt; [allowNoIndex](./kibana-plugin-plugins-data-public.getfieldsoptions.allownoindex.md)
## GetFieldsOptions.allowNoIndex property
<b>Signature:</b>
```typescript
allowNoIndex?: boolean;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) &gt; [lookBack](./kibana-plugin-plugins-data-public.getfieldsoptions.lookback.md)
## GetFieldsOptions.lookBack property
<b>Signature:</b>
```typescript
lookBack?: boolean;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [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> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) &gt; [metaFields](./kibana-plugin-plugins-data-public.getfieldsoptions.metafields.md)
## GetFieldsOptions.metaFields property
<b>Signature:</b>
```typescript
metaFields?: string[];
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) &gt; [pattern](./kibana-plugin-plugins-data-public.getfieldsoptions.pattern.md)
## GetFieldsOptions.pattern property
<b>Signature:</b>
```typescript
pattern: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) &gt; [rollupIndex](./kibana-plugin-plugins-data-public.getfieldsoptions.rollupindex.md)
## GetFieldsOptions.rollupIndex property
<b>Signature:</b>
```typescript
rollupIndex?: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [GetFieldsOptions](./kibana-plugin-plugins-data-public.getfieldsoptions.md) &gt; [type](./kibana-plugin-plugins-data-public.getfieldsoptions.type.md)
## GetFieldsOptions.type property
<b>Signature:</b>
```typescript
type?: string;
```

View file

@ -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) | |

View file

@ -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

View file

@ -84,6 +84,7 @@ export interface OverlayFlyoutOpenOptions {
'data-test-subj'?: string;
size?: EuiFlyoutSize;
maxWidth?: boolean | number | string;
hideCloseButton?: boolean;
}
interface StartDeps {

View file

@ -1027,6 +1027,8 @@ export interface OverlayFlyoutOpenOptions {
// (undocumented)
closeButtonAriaLabel?: string;
// (undocumented)
hideCloseButton?: boolean;
// (undocumented)
maxWidth?: boolean | number | string;
// (undocumented)
ownFocus?: boolean;

View file

@ -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!;
}
/**

View file

@ -83,7 +83,9 @@ export {
IndexPatternSpec,
IndexPatternLoadExpressionFunctionDefinition,
fieldList,
GetFieldsOptions,
INDEX_PATTERN_SAVED_OBJECT_TYPE,
AggregationRestrictions,
IndexPatternType,
} from '../common';

View file

@ -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)

View 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}
/>
```

View 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'],
};

View 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"
}

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
$inpEmptyStateMaxWidth: $euiSizeXXL * 19;

View file

@ -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>
);

View file

@ -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>
</>
);
};

View file

@ -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';

View file

@ -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>
`;

View file

@ -1,5 +1,5 @@
@import '../../../variables';
@import '../../../templates';
@import '../../variables';
@import '../../templates';
.inpEmptyState {
// override EUI specificity

View file

@ -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}
/>
);

View file

@ -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}
</>
);
};

View file

@ -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';

View file

@ -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>

View file

@ -537,4 +537,5 @@ const IndexPatternIllustration = () => (
</svg>
);
export const Illustration = IndexPatternIllustration;
/* eslint-disable import/no-default-export */
export default IndexPatternIllustration;

View file

@ -1,5 +1,5 @@
@import '../../../variables';
@import '../../../templates';
@import '../../variables';
@import '../../templates';
.inpEmptyIndexPatternPrompt {
// override EUI specificity

View file

@ -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/'}
/>
);

View file

@ -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>

View 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, { 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}</>;
};

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { LoadingState } from './loading_state';
export { EmptyPrompts } from './empty_prompts';

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { Header } from './header';
export { PromptFooter } from './prompt_footer';

View file

@ -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>
);
};

View file

@ -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;
};

View file

@ -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;
}
}

View file

@ -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;
};

View file

@ -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} />;
};

View file

@ -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} />;
};

View file

@ -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 />
</>
);

View file

@ -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,
};

View file

@ -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>
);
};

View 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 { TimestampField } from './timestamp_field';
export { TypeField } from './type_field';
export { TitleField } from './title_field';

View file

@ -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 || <>&nbsp;</>}
</EuiFormHelpText>
</>
</EuiFormRow>
</>
);
}}
</UseField>
);
};

View file

@ -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>
);
};

View file

@ -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"
/>
&nbsp;
<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>
);
};

View file

@ -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,
},
};

View file

@ -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',
}),
};
};

View 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';

View file

@ -0,0 +1,13 @@
.indexPatternEditor__form {
flex-grow: 1;
}
.fieldEditor__mainFlyoutPanel {
display: flex;
flex-direction: column;
}
.indexPatternEditor__footer {
margin-left: -$euiSizeL;
margin-right: -$euiSizeL;
}

View file

@ -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>
);
};

View file

@ -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);

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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>
);

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { CreateButton } from './create_button';
export { PreviewPanel } from './preview_panel';

View file

@ -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,

View file

@ -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: [] },

View file

@ -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 }}
/>

View file

@ -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}
</>
);
};

View file

@ -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>

View file

@ -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={''}

View file

@ -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>
&nbsp;
<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 (

View file

@ -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';

View file

@ -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>
);

View 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';

View 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();
}

View file

@ -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();
});
});

View file

@ -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;

View file

@ -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: '' },
]);
});
});

View file

@ -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;
}

View file

@ -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);
});
});

View file

@ -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' });
}

View file

@ -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,

View 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,
};

View 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);
};

View 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');
});
});

View 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