({
search: queryString ? `?${queryString}` : queryString,
},
route: {
- settings: { tags: routeTags, auth: routeAuthRequired },
+ settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteState },
},
raw: {
req: { socket },
@@ -109,6 +112,7 @@ function createRawRequestMock(customization: DeepPartial = {}) {
return merge(
{},
{
+ app: { xsrfRequired: true } as any,
headers: {},
path: '/',
route: { settings: {} },
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index a9fc80c86d878..27db79bb94d25 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -811,6 +811,7 @@ test('exposes route details of incoming request to a route handler', async () =>
path: '/',
options: {
authRequired: true,
+ xsrfRequired: false,
tags: [],
},
});
@@ -923,6 +924,7 @@ test('exposes route details of incoming request to a route handler (POST + paylo
path: '/',
options: {
authRequired: true,
+ xsrfRequired: true,
tags: [],
body: {
parse: true, // hapi populates the default
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index 025ab2bf56ac2..cffdffab0d0cf 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -27,7 +27,7 @@ import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_p
import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response';
-import { IRouter } from './router';
+import { IRouter, KibanaRouteState, isSafeMethod } from './router';
import {
SessionStorageCookieOptions,
createCookieSessionStorageFactory,
@@ -147,9 +147,14 @@ export class HttpServer {
for (const route of router.getRoutes()) {
this.log.debug(`registering route handler for [${route.path}]`);
// Hapi does not allow payload validation to be specified for 'head' or 'get' requests
- const validate = ['head', 'get'].includes(route.method) ? undefined : { payload: true };
+ const validate = isSafeMethod(route.method) ? undefined : { payload: true };
const { authRequired = true, tags, body = {} } = route.options;
const { accepts: allow, maxBytes, output, parse } = body;
+
+ const kibanaRouteState: KibanaRouteState = {
+ xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method),
+ };
+
this.server.route({
handler: route.handler,
method: route.method,
@@ -157,6 +162,7 @@ export class HttpServer {
options: {
// Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }`
auth: authRequired === true ? undefined : false,
+ app: kibanaRouteState,
tags: tags ? Array.from(tags) : undefined,
// TODO: This 'validate' section can be removed once the legacy platform is completely removed.
// We are telling Hapi that NP routes can accept any payload, so that it can bypass the default
diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts
index d31afe1670e41..8f4c02680f8a3 100644
--- a/src/core/server/http/index.ts
+++ b/src/core/server/http/index.ts
@@ -58,6 +58,8 @@ export {
RouteValidationError,
RouteValidatorFullConfig,
RouteValidationResultFactory,
+ DestructiveRouteMethod,
+ SafeRouteMethod,
} from './router';
export { BasePathProxyServer } from './base_path_proxy_server';
export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth';
diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
index f4c5f16870c7e..b5364c616f17c 100644
--- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
+++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
@@ -36,6 +36,7 @@ const versionHeader = 'kbn-version';
const xsrfHeader = 'kbn-xsrf';
const nameHeader = 'kbn-name';
const whitelistedTestPath = '/xsrf/test/route/whitelisted';
+const xsrfDisabledTestPath = '/xsrf/test/route/disabled';
const kibanaName = 'my-kibana-name';
const setupDeps = {
context: contextServiceMock.createSetupContract(),
@@ -188,6 +189,12 @@ describe('core lifecycle handlers', () => {
return res.ok({ body: 'ok' });
}
);
+ ((router as any)[method.toLowerCase()] as RouteRegistrar)(
+ { path: xsrfDisabledTestPath, validate: false, options: { xsrfRequired: false } },
+ (context, req, res) => {
+ return res.ok({ body: 'ok' });
+ }
+ );
});
await server.start();
@@ -235,6 +242,10 @@ describe('core lifecycle handlers', () => {
it('accepts whitelisted requests without either an xsrf or version header', async () => {
await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok');
});
+
+ it('accepts requests on a route with disabled xsrf protection', async () => {
+ await getSupertest(method.toLowerCase(), xsrfDisabledTestPath).expect(200, 'ok');
+ });
});
});
});
diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts
index 48a6973b741ba..a80e432e0d4cb 100644
--- a/src/core/server/http/lifecycle_handlers.test.ts
+++ b/src/core/server/http/lifecycle_handlers.test.ts
@@ -24,7 +24,7 @@ import {
} from './lifecycle_handlers';
import { httpServerMock } from './http_server.mocks';
import { HttpConfig } from './http_config';
-import { KibanaRequest, RouteMethod } from './router';
+import { KibanaRequest, RouteMethod, KibanaRouteState } from './router';
const createConfig = (partial: Partial): HttpConfig => partial as HttpConfig;
@@ -32,12 +32,14 @@ const forgeRequest = ({
headers = {},
path = '/',
method = 'get',
+ kibanaRouteState,
}: Partial<{
headers: Record;
path: string;
method: RouteMethod;
+ kibanaRouteState: KibanaRouteState;
}>): KibanaRequest => {
- return httpServerMock.createKibanaRequest({ headers, path, method });
+ return httpServerMock.createKibanaRequest({ headers, path, method, kibanaRouteState });
};
describe('xsrf post-auth handler', () => {
@@ -142,6 +144,29 @@ describe('xsrf post-auth handler', () => {
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
+
+ it('accepts requests if xsrf protection on a route is disabled', () => {
+ const config = createConfig({
+ xsrf: { whitelist: [], disableProtection: false },
+ });
+ const handler = createXsrfPostAuthHandler(config);
+ const request = forgeRequest({
+ method: 'post',
+ headers: {},
+ path: '/some-path',
+ kibanaRouteState: {
+ xsrfRequired: false,
+ },
+ });
+
+ toolkit.next.mockReturnValue('next' as any);
+
+ const result = handler(request, responseFactory, toolkit);
+
+ expect(responseFactory.badRequest).not.toHaveBeenCalled();
+ expect(toolkit.next).toHaveBeenCalledTimes(1);
+ expect(result).toEqual('next');
+ });
});
});
diff --git a/src/core/server/http/lifecycle_handlers.ts b/src/core/server/http/lifecycle_handlers.ts
index ee877ee031a2b..7ef7e86326039 100644
--- a/src/core/server/http/lifecycle_handlers.ts
+++ b/src/core/server/http/lifecycle_handlers.ts
@@ -20,6 +20,7 @@
import { OnPostAuthHandler } from './lifecycle/on_post_auth';
import { OnPreResponseHandler } from './lifecycle/on_pre_response';
import { HttpConfig } from './http_config';
+import { isSafeMethod } from './router';
import { Env } from '../config';
import { LifecycleRegistrar } from './http_server';
@@ -31,15 +32,18 @@ export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler
const { whitelist, disableProtection } = config.xsrf;
return (request, response, toolkit) => {
- if (disableProtection || whitelist.includes(request.route.path)) {
+ if (
+ disableProtection ||
+ whitelist.includes(request.route.path) ||
+ request.route.options.xsrfRequired === false
+ ) {
return toolkit.next();
}
- const isSafeMethod = request.route.method === 'get' || request.route.method === 'head';
const hasVersionHeader = VERSION_HEADER in request.headers;
const hasXsrfHeader = XSRF_HEADER in request.headers;
- if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) {
+ if (!isSafeMethod(request.route.method) && !hasVersionHeader && !hasXsrfHeader) {
return response.badRequest({ body: `Request must contain a ${XSRF_HEADER} header.` });
}
diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts
index 32663d1513f36..d254f391ca5e4 100644
--- a/src/core/server/http/router/index.ts
+++ b/src/core/server/http/router/index.ts
@@ -24,16 +24,20 @@ export {
KibanaRequestEvents,
KibanaRequestRoute,
KibanaRequestRouteOptions,
+ KibanaRouteState,
isRealRequest,
LegacyRequest,
ensureRawRequest,
} from './request';
export {
+ DestructiveRouteMethod,
+ isSafeMethod,
RouteMethod,
RouteConfig,
RouteConfigOptions,
RouteContentType,
RouteConfigOptionsBody,
+ SafeRouteMethod,
validBodyOutput,
} from './route';
export { HapiResponseAdapter } from './response_adapter';
diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts
index 703571ba53c0a..bb2db6367f701 100644
--- a/src/core/server/http/router/request.ts
+++ b/src/core/server/http/router/request.ts
@@ -18,18 +18,24 @@
*/
import { Url } from 'url';
-import { Request } from 'hapi';
+import { Request, ApplicationState } from 'hapi';
import { Observable, fromEvent, merge } from 'rxjs';
import { shareReplay, first, takeUntil } from 'rxjs/operators';
import { deepFreeze, RecursiveReadonly } from '../../../utils';
import { Headers } from './headers';
-import { RouteMethod, RouteConfigOptions, validBodyOutput } from './route';
+import { RouteMethod, RouteConfigOptions, validBodyOutput, isSafeMethod } from './route';
import { KibanaSocket, IKibanaSocket } from './socket';
import { RouteValidator, RouteValidatorFullConfig } from './validator';
const requestSymbol = Symbol('request');
+/**
+ * @internal
+ */
+export interface KibanaRouteState extends ApplicationState {
+ xsrfRequired: boolean;
+}
/**
* Route options: If 'GET' or 'OPTIONS' method, body options won't be returned.
* @public
@@ -184,8 +190,10 @@ export class KibanaRequest<
const options = ({
authRequired: request.route.settings.auth !== false,
+ // some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8
+ xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true,
tags: request.route.settings.tags || [],
- body: ['get', 'options'].includes(method)
+ body: isSafeMethod(method)
? undefined
: {
parse,
diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts
index 4439a80b1eac7..d1458ef4ad063 100644
--- a/src/core/server/http/router/route.ts
+++ b/src/core/server/http/router/route.ts
@@ -19,11 +19,27 @@
import { RouteValidatorFullConfig } from './validator';
+export function isSafeMethod(method: RouteMethod): method is SafeRouteMethod {
+ return method === 'get' || method === 'options';
+}
+
+/**
+ * Set of HTTP methods changing the state of the server.
+ * @public
+ */
+export type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch';
+
+/**
+ * Set of HTTP methods not changing the state of the server.
+ * @public
+ */
+export type SafeRouteMethod = 'get' | 'options';
+
/**
* The set of common HTTP methods supported by Kibana routing.
* @public
*/
-export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options';
+export type RouteMethod = SafeRouteMethod | DestructiveRouteMethod;
/**
* The set of valid body.output
@@ -108,6 +124,15 @@ export interface RouteConfigOptions {
*/
authRequired?: boolean;
+ /**
+ * Defines xsrf protection requirements for a route:
+ * - true. Requires an incoming POST/PUT/DELETE request to contain `kbn-xsrf` header.
+ * - false. Disables xsrf protection.
+ *
+ * Set to true by default
+ */
+ xsrfRequired?: Method extends 'get' ? never : boolean;
+
/**
* Additional metadata tag strings to attach to the route.
*/
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index de6cdb2d7acd7..0c112e3cfb5b2 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -159,6 +159,8 @@ export {
SessionStorageCookieOptions,
SessionCookieValidationResult,
SessionStorageFactory,
+ DestructiveRouteMethod,
+ SafeRouteMethod,
} from './http';
export { RenderingServiceSetup, IRenderOptions } from './rendering';
export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 445ed16ec7829..8c5e84446a0d3 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -685,6 +685,9 @@ export interface DeprecationSettings {
message: string;
}
+// @public
+export type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch';
+
// @public
export interface DiscoveredPlugin {
readonly configPath: ConfigPath;
@@ -1459,6 +1462,7 @@ export interface RouteConfigOptions {
authRequired?: boolean;
body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody;
tags?: readonly string[];
+ xsrfRequired?: Method extends 'get' ? never : boolean;
}
// @public
@@ -1473,7 +1477,7 @@ export interface RouteConfigOptionsBody {
export type RouteContentType = 'application/json' | 'application/*+json' | 'application/octet-stream' | 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/*';
// @public
-export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options';
+export type RouteMethod = SafeRouteMethod | DestructiveRouteMethod;
// @public
export type RouteRegistrar =
(route: RouteConfig
, handler: RequestHandler
) => void;
@@ -1526,6 +1530,9 @@ export interface RouteValidatorOptions {
};
}
+// @public
+export type SafeRouteMethod = 'get' | 'options';
+
// @public (undocumented)
export interface SavedObject {
attributes: T;
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/documentation_links.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/documentation_links.js
index ce42b26cc3e86..bc9cb15e1c5e0 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/services/documentation_links.js
+++ b/x-pack/legacy/plugins/rollup/public/crud_app/services/documentation_links.js
@@ -5,11 +5,9 @@
*/
let esBase = '';
-let xPackBase = '';
export function setEsBaseAndXPackBase(elasticWebsiteUrl, docLinksVersion) {
esBase = `${elasticWebsiteUrl}guide/en/elasticsearch/reference/${docLinksVersion}`;
- xPackBase = `${elasticWebsiteUrl}guide/en/x-pack/${docLinksVersion}`;
}
export const getLogisticalDetailsUrl = () => `${esBase}/rollup-job-config.html#_logistical_details`;
@@ -21,4 +19,4 @@ export const getMetricsDetailsUrl = () => `${esBase}/rollup-job-config.html#roll
export const getDateHistogramAggregationUrl = () =>
`${esBase}/search-aggregations-bucket-datehistogram-aggregation.html`;
-export const getCronUrl = () => `${xPackBase}/trigger-schedule.html#_cron_expressions`;
+export const getCronUrl = () => `${esBase}/trigger-schedule.html#_cron_expressions`;
diff --git a/x-pack/legacy/plugins/transform/public/app/common/data_grid.ts b/x-pack/legacy/plugins/transform/public/app/common/data_grid.ts
new file mode 100644
index 0000000000000..0783839afee83
--- /dev/null
+++ b/x-pack/legacy/plugins/transform/public/app/common/data_grid.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiDataGridStyle } from '@elastic/eui';
+
+export const euiDataGridStyle: EuiDataGridStyle = {
+ border: 'all',
+ fontSize: 's',
+ cellPadding: 's',
+ stripes: false,
+ rowHover: 'highlight',
+ header: 'shade',
+};
+
+export const euiDataGridToolbarSettings = {
+ showColumnSelector: true,
+ showStyleSelector: false,
+ showSortSelector: true,
+ showFullScreenSelector: false,
+};
diff --git a/x-pack/legacy/plugins/transform/public/app/common/fields.ts b/x-pack/legacy/plugins/transform/public/app/common/fields.ts
index f2181654286db..108f45ce67e37 100644
--- a/x-pack/legacy/plugins/transform/public/app/common/fields.ts
+++ b/x-pack/legacy/plugins/transform/public/app/common/fields.ts
@@ -15,8 +15,6 @@ export interface EsDoc extends Dictionary {
_source: EsDocSource;
}
-export const MAX_COLUMNS = 5;
-
export function getFlattenedFields(obj: EsDocSource): EsFieldName[] {
const flatDocFields: EsFieldName[] = [];
const newDocFields = Object.keys(obj);
@@ -33,35 +31,33 @@ export function getFlattenedFields(obj: EsDocSource): EsFieldName[] {
return flatDocFields;
}
-export const getSelectableFields = (docs: EsDoc[]): EsFieldName[] => {
+export const getSelectableFields = (docs: EsDocSource[]): EsFieldName[] => {
if (docs.length === 0) {
return [];
}
- const newDocFields = getFlattenedFields(docs[0]._source);
+ const newDocFields = getFlattenedFields(docs[0]);
newDocFields.sort();
return newDocFields;
};
-export const getDefaultSelectableFields = (docs: EsDoc[]): EsFieldName[] => {
+export const getDefaultSelectableFields = (docs: EsDocSource[]): EsFieldName[] => {
if (docs.length === 0) {
return [];
}
- const newDocFields = getFlattenedFields(docs[0]._source);
+ const newDocFields = getFlattenedFields(docs[0]);
newDocFields.sort();
- return newDocFields
- .filter(k => {
- let value = false;
- docs.forEach(row => {
- const source = row._source;
- if (source[k] !== null) {
- value = true;
- }
- });
- return value;
- })
- .slice(0, MAX_COLUMNS);
+ return newDocFields.filter(k => {
+ let value = false;
+ docs.forEach(row => {
+ const source = row;
+ if (source[k] !== null) {
+ value = true;
+ }
+ });
+ return value;
+ });
};
export const toggleSelectedField = (
diff --git a/x-pack/legacy/plugins/transform/public/app/common/index.ts b/x-pack/legacy/plugins/transform/public/app/common/index.ts
index 3f515db389b45..52a6884367bc5 100644
--- a/x-pack/legacy/plugins/transform/public/app/common/index.ts
+++ b/x-pack/legacy/plugins/transform/public/app/common/index.ts
@@ -5,6 +5,7 @@
*/
export { AggName, isAggName } from './aggregations';
+export { euiDataGridStyle, euiDataGridToolbarSettings } from './data_grid';
export {
getDefaultSelectableFields,
getFlattenedFields,
@@ -13,7 +14,6 @@ export {
EsDoc,
EsDocSource,
EsFieldName,
- MAX_COLUMNS,
} from './fields';
export { DropDownLabel, DropDownOption, Label } from './dropdown';
export {
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx
index 3268d6697ed44..76ed12ff772f5 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx
@@ -4,59 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState } from 'react';
-import moment from 'moment-timezone';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
- EuiBadge,
- EuiButtonEmpty,
EuiButtonIcon,
EuiCallOut,
- EuiCheckbox,
EuiCodeBlock,
EuiCopy,
+ EuiDataGrid,
EuiFlexGroup,
EuiFlexItem,
- EuiPanel,
- EuiPopover,
- EuiPopoverTitle,
EuiProgress,
- EuiText,
EuiTitle,
- EuiToolTip,
- RIGHT_ALIGNMENT,
} from '@elastic/eui';
-import {
- ColumnType,
- mlInMemoryTableBasicFactory,
- SortingPropType,
- SORT_DIRECTION,
-} from '../../../../../shared_imports';
-
-import { KBN_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
-import { Dictionary } from '../../../../../../common/types/common';
-import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils';
+import { getNestedProperty } from '../../../../../../common/utils/object_utils';
import {
- toggleSelectedField,
- EsDoc,
+ euiDataGridStyle,
+ euiDataGridToolbarSettings,
EsFieldName,
- MAX_COLUMNS,
PivotQuery,
} from '../../../../common';
import { SearchItems } from '../../../../hooks/use_search_items';
import { getSourceIndexDevConsoleStatement } from './common';
-import { ExpandedRow } from './expanded_row';
import { SOURCE_INDEX_STATUS, useSourceIndexData } from './use_source_index_data';
-type ItemIdToExpandedRowMap = Dictionary;
-
-const CELL_CLICK_ENABLED = false;
-
interface SourceIndexPreviewTitle {
indexPatternTitle: string;
}
@@ -74,360 +50,196 @@ const SourceIndexPreviewTitle: React.FC = ({ indexPatte
interface Props {
indexPattern: SearchItems['indexPattern'];
query: PivotQuery;
- cellClick?(search: string): void;
}
-export const SourceIndexPreview: React.FC = React.memo(
- ({ indexPattern, cellClick, query }) => {
- const [clearTable, setClearTable] = useState(false);
-
- const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
- const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
-
- // EuiInMemoryTable has an issue with dynamic sortable columns
- // and will trigger a full page Kibana error in such a case.
- // The following is a workaround until this is solved upstream:
- // - If the sortable/columns config changes,
- // the table will be unmounted/not rendered.
- // This is what setClearTable(true) in toggleColumn() does.
- // - After that on next render it gets re-enabled. To make sure React
- // doesn't consolidate the state updates, setTimeout is used.
- if (clearTable) {
- setTimeout(() => setClearTable(false), 0);
- }
+const defaultPagination = { pageIndex: 0, pageSize: 5 };
- function toggleColumnsPopover() {
- setColumnsPopoverVisible(!isColumnsPopoverVisible);
+export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, query }) => {
+ const allFields = indexPattern.fields.map(f => f.name);
+ const indexPatternFields: string[] = allFields.filter(f => {
+ if (indexPattern.metaFields.includes(f)) {
+ return false;
}
- function closeColumnsPopover() {
- setColumnsPopoverVisible(false);
+ const fieldParts = f.split('.');
+ const lastPart = fieldParts.pop();
+ if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) {
+ return false;
}
- function toggleColumn(column: EsFieldName) {
- // spread to a new array otherwise the component wouldn't re-render
- setClearTable(true);
- setSelectedFields([...toggleSelectedField(selectedFields, column)]);
- }
+ return true;
+ });
- const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState(
- {} as ItemIdToExpandedRowMap
- );
+ // Column visibility
+ const [visibleColumns, setVisibleColumns] = useState(indexPatternFields);
- function toggleDetails(item: EsDoc) {
- if (itemIdToExpandedRowMap[item._id]) {
- delete itemIdToExpandedRowMap[item._id];
- } else {
- itemIdToExpandedRowMap[item._id] = ;
- }
- // spread to a new object otherwise the component wouldn't re-render
- setItemIdToExpandedRowMap({ ...itemIdToExpandedRowMap });
- }
+ const [pagination, setPagination] = useState(defaultPagination);
- const { errorMessage, status, tableItems } = useSourceIndexData(
- indexPattern,
- query,
- selectedFields,
- setSelectedFields
- );
+ useEffect(() => {
+ setPagination(defaultPagination);
+ }, [query]);
- if (status === SOURCE_INDEX_STATUS.ERROR) {
- return (
-
-
-
-
- {errorMessage}
-
-
-
- );
- }
+ const { errorMessage, status, rowCount, tableItems: data } = useSourceIndexData(
+ indexPattern,
+ query,
+ pagination
+ );
- if (status === SOURCE_INDEX_STATUS.LOADED && tableItems.length === 0) {
- return (
-
-
-
-
- {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody', {
- defaultMessage:
- 'The query for the source index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.',
- })}
-
-
-
- );
- }
+ // EuiDataGrid State
+ const dataGridColumns = indexPatternFields.map(id => {
+ const field = indexPattern.fields.getByName(id);
- let docFields: EsFieldName[] = [];
- let docFieldsCount = 0;
- if (tableItems.length > 0) {
- docFields = Object.keys(tableItems[0]._source);
- docFields.sort();
- docFieldsCount = docFields.length;
+ let schema = 'string';
+
+ switch (field?.type) {
+ case 'date':
+ schema = 'datetime';
+ break;
+ case 'geo_point':
+ schema = 'json';
+ break;
+ case 'number':
+ schema = 'numeric';
+ break;
}
- const columns: Array> = selectedFields.map(k => {
- const column: ColumnType = {
- field: `_source["${k}"]`,
- name: k,
- sortable: true,
- truncateText: true,
- };
-
- const field = indexPattern.fields.find(f => f.name === k);
-
- const formatField = (d: string) => {
- return field !== undefined && field.type === KBN_FIELD_TYPES.DATE
- ? formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000)
- : d;
- };
-
- const render = (d: any) => {
- if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
- // If the cells data is an array of strings, return as a comma separated list.
- // The list will get limited to 5 items with `…` at the end if there's more in the original array.
- return `${d
- .map(item => formatField(item))
- .slice(0, 5)
- .join(', ')}${d.length > 5 ? ', …' : ''}`;
- } else if (Array.isArray(d)) {
- // If the cells data is an array of e.g. objects, display a 'array' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexArrayBadgeContent', {
- defaultMessage: 'array',
- })}
-
-
- );
- } else if (typeof d === 'object' && d !== null) {
- // If the cells data is an object, display a 'object' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate(
- 'xpack.transform.sourceIndexPreview.SourceIndexObjectBadgeContent',
- {
- defaultMessage: 'object',
- }
- )}
-
-
- );
- }
-
- return formatField(d);
- };
-
- if (typeof field !== 'undefined') {
- switch (field.type) {
- case KBN_FIELD_TYPES.BOOLEAN:
- column.dataType = 'boolean';
- break;
- case KBN_FIELD_TYPES.DATE:
- column.align = 'right';
- column.render = (d: any) => formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
- break;
- case KBN_FIELD_TYPES.NUMBER:
- column.dataType = 'number';
- break;
- default:
- column.render = render;
- break;
- }
- } else {
- column.render = render;
+ return { id, schema };
+ });
+
+ const onChangeItemsPerPage = useCallback(
+ pageSize => {
+ setPagination(p => {
+ const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize);
+ return { pageIndex, pageSize };
+ });
+ },
+ [setPagination]
+ );
+
+ const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [
+ setPagination,
+ ]);
+
+ // ** Sorting config
+ const [sortingColumns, setSortingColumns] = useState([]);
+ const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]);
+
+ const renderCellValue = useMemo(() => {
+ return ({
+ rowIndex,
+ columnId,
+ setCellProps,
+ }: {
+ rowIndex: number;
+ columnId: string;
+ setCellProps: any;
+ }) => {
+ const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize;
+
+ const cellValue = data.hasOwnProperty(adjustedRowIndex)
+ ? getNestedProperty(data[adjustedRowIndex], columnId, null)
+ : null;
+
+ if (typeof cellValue === 'object' && cellValue !== null) {
+ return JSON.stringify(cellValue);
}
- if (CELL_CLICK_ENABLED && cellClick) {
- column.render = (d: string) => (
- cellClick(`${k}:(${d})`)}>
- {render(d)}
-
- );
+ if (cellValue === undefined) {
+ return null;
}
- return column;
- });
-
- let sorting: SortingPropType = false;
+ return cellValue;
+ };
+ }, [data, pagination.pageIndex, pagination.pageSize]);
- if (columns.length > 0) {
- sorting = {
- sort: {
- field: `_source["${selectedFields[0]}"]`,
- direction: SORT_DIRECTION.ASC,
- },
- };
- }
-
- columns.unshift({
- align: RIGHT_ALIGNMENT,
- width: '40px',
- isExpander: true,
- render: (item: EsDoc) => (
- toggleDetails(item)}
- aria-label={
- itemIdToExpandedRowMap[item._id]
- ? i18n.translate('xpack.transform.sourceIndexPreview.rowCollapse', {
- defaultMessage: 'Collapse',
- })
- : i18n.translate('xpack.transform.sourceIndexPreview.rowExpand', {
- defaultMessage: 'Expand',
- })
- }
- iconType={itemIdToExpandedRowMap[item._id] ? 'arrowUp' : 'arrowDown'}
- />
- ),
- });
+ if (status === SOURCE_INDEX_STATUS.ERROR) {
+ return (
+
+
+
+
+ {errorMessage}
+
+
+
+ );
+ }
- const euiCopyText = i18n.translate('xpack.transform.sourceIndexPreview.copyClipboardTooltip', {
- defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.',
- });
+ if (status === SOURCE_INDEX_STATUS.LOADED && data.length === 0) {
+ return (
+
+
+
+
+ {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody', {
+ defaultMessage:
+ 'The query for the source index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.',
+ })}
+
-
-
-
+
+
+ ({
- text: i18n.translate('xpack.uptime.breadcrumbs.overviewBreadcrumbText', {
- defaultMessage: 'Uptime',
- }),
- href: `#/${search ? search : ''}`,
-});
-
-export const getOverviewPageBreadcrumbs = (search?: string): ChromeBreadcrumb[] => [
- makeOverviewBreadcrumb(search),
-];
-
-export const getMonitorPageBreadcrumb = (name: string, search?: string): ChromeBreadcrumb[] => [
- makeOverviewBreadcrumb(search),
- { text: name },
-];
diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/pages/page_header_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/pages/page_header_container.tsx
deleted file mode 100644
index 9429b87061ff7..0000000000000
--- a/x-pack/legacy/plugins/uptime/public/components/connected/pages/page_header_container.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { connect } from 'react-redux';
-import { selectSelectedMonitor } from '../../../state/selectors';
-import { AppState } from '../../../state';
-import { PageHeaderComponent } from '../../../pages/page_header';
-
-const mapStateToProps = (state: AppState) => ({
- monitorStatus: selectSelectedMonitor(state),
-});
-
-export const PageHeader = connect(mapStateToProps, null)(PageHeaderComponent);
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap
new file mode 100644
index 0000000000000..79ef7b3b97abd
--- /dev/null
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ChartEmptyState renders JSX values 1`] = `
+
+
+ down
+ ,
+ }
+ }
+ />
+
+ }
+ title={
+
+
+
+
+
+ }
+/>
+`;
+
+exports[`ChartEmptyState renders string values 1`] = `
+
+ This is the body
+
+ }
+ title={
+
+
+ This is the title
+
+
+ }
+/>
+`;
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_empty_state.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_empty_state.test.tsx
new file mode 100644
index 0000000000000..2e25dddc0b4ed
--- /dev/null
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_empty_state.test.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ChartEmptyState } from '../chart_empty_state';
+import { shallowWithIntl } from 'test_utils/enzyme_helpers';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+describe('ChartEmptyState', () => {
+ it('renders string values', () => {
+ expect(
+ shallowWithIntl()
+ ).toMatchSnapshot();
+ });
+
+ it('renders JSX values', () => {
+ expect(
+ shallowWithIntl(
+ down }}
+ />
+ }
+ title={}
+ />
+ )
+ ).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_empty_state.tsx
new file mode 100644
index 0000000000000..19202822fe737
--- /dev/null
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_empty_state.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiEmptyPrompt, EuiTitle } from '@elastic/eui';
+import React, { FC } from 'react';
+
+interface ChartEmptyStateProps {
+ title: string | JSX.Element;
+ body: string | JSX.Element;
+}
+
+export const ChartEmptyState: FC = ({ title, body }) => (
+
+
{title}
+
+ }
+ body={
{body}
}
+ />
+);
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/checks_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/checks_chart.tsx
deleted file mode 100644
index a88a9668660f7..0000000000000
--- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/checks_chart.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import {
- AreaSeries,
- Axis,
- Chart,
- Position,
- Settings,
- ScaleType,
- timeFormatter,
-} from '@elastic/charts';
-import { EuiPanel, EuiTitle } from '@elastic/eui';
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { StatusData } from '../../../../common/graphql/types';
-import { getChartDateLabel } from '../../../lib/helper';
-import { useUrlParams } from '../../../hooks';
-
-interface ChecksChartProps {
- /**
- * The color that will be used for the area series displaying "Down" checks.
- */
- dangerColor: string;
- /**
- * The timeseries data displayed in the chart.
- */
- status: StatusData[];
- /**
- * The color that will be used for the area series displaying "Up" checks.
- */
- successColor: string;
-}
-
-/**
- * Renders a chart that displays the total count of up/down status checks over time
- * as a stacked area chart.
- * @param props The props values required by this component.
- */
-export const ChecksChart = ({ dangerColor, status, successColor }: ChecksChartProps) => {
- const upSeriesSpecId = 'Up';
- const downSeriesSpecId = 'Down';
- const [getUrlParams] = useUrlParams();
- const { absoluteDateRangeStart: min, absoluteDateRangeEnd: max } = getUrlParams();
-
- const upString = i18n.translate('xpack.uptime.monitorCharts.checkStatus.series.upCountLabel', {
- defaultMessage: 'Up count',
- });
- const downString = i18n.translate(
- 'xpack.uptime.monitorCharts.checkStatus.series.downCountLabel',
- {
- defaultMessage: 'Down count',
- }
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
- Number(d).toFixed(0)}
- title={i18n.translate('xpack.uptime.monitorChart.checksChart.leftAxis.title', {
- defaultMessage: 'Number of checks',
- description: 'The heading of the y-axis of a chart of timeseries data',
- })}
- />
- ({
- x,
- [upString]: up || 0,
- }))}
- id={upSeriesSpecId}
- stackAccessors={['x']}
- timeZone="local"
- xAccessor="x"
- xScaleType={ScaleType.Time}
- yAccessors={[upString]}
- yScaleType={ScaleType.Linear}
- />
- ({
- x,
- [downString]: down || 0,
- }))}
- id={downSeriesSpecId}
- stackAccessors={['x']}
- timeZone="local"
- xAccessor="x"
- xScaleType={ScaleType.Time}
- yAccessors={[downString]}
- yScaleType={ScaleType.Linear}
- />
-
-
-
- );
-};
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx
index 7a6db6d952dd9..0488e2531bc98 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx
@@ -13,10 +13,10 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { getChartDateLabel } from '../../../lib/helper';
import { LocationDurationLine } from '../../../../common/graphql/types';
import { DurationLineSeriesList } from './duration_line_series_list';
-import { DurationChartEmptyState } from './duration_chart_empty_state';
import { ChartWrapper } from './chart_wrapper';
import { useUrlParams } from '../../../hooks';
import { getTickFormat } from './get_tick_format';
+import { ChartEmptyState } from './chart_empty_state';
interface DurationChartProps {
/**
@@ -102,7 +102,18 @@ export const DurationChart = ({
) : (
-
+ up }}
+ />
+ }
+ title={i18n.translate('xpack.uptime.durationChart.emptyPrompt.title', {
+ defaultMessage: 'No duration data available',
+ })}
+ />
)}
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart_empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart_empty_state.tsx
deleted file mode 100644
index ef4e70bf65898..0000000000000
--- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart_empty_state.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiEmptyPrompt, EuiTitle } from '@elastic/eui';
-import React from 'react';
-
-export const DurationChartEmptyState = () => (
-
-
-
-
-
- }
- body={
-
- up }}
- />
-
- }
- />
-);
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/ping_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/ping_histogram.tsx
index b4989282f854c..6119d897cbf53 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/ping_histogram.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/ping_histogram.tsx
@@ -5,7 +5,7 @@
*/
import { Axis, BarSeries, Chart, Position, Settings, timeFormatter } from '@elastic/charts';
-import { EuiEmptyPrompt, EuiPanel, EuiTitle } from '@elastic/eui';
+import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -15,6 +15,7 @@ import { ChartWrapper } from './chart_wrapper';
import { UptimeThemeContext } from '../../../contexts';
import { HistogramResult } from '../../../../common/types';
import { useUrlParams } from '../../../hooks';
+import { ChartEmptyState } from './chart_empty_state';
export interface PingHistogramComponentProps {
/**
@@ -49,71 +50,36 @@ export const PingHistogramComponent: React.FC = ({
const [, updateUrlParams] = useUrlParams();
- if (!data || !data.histogram)
- /**
- * TODO: the Fragment, EuiTitle, and EuiPanel should be extracted to a dumb component
- * that we can reuse in the subsequent return statement at the bottom of this function.
- */
- return (
- <>
-
-