mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
parent
58363884ac
commit
db8b86538a
436 changed files with 27376 additions and 59 deletions
|
@ -89,6 +89,8 @@
|
|||
"@kbn/pm": "link:packages/kbn-pm",
|
||||
"@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector",
|
||||
"@kbn/ui-framework": "link:packages/kbn-ui-framework",
|
||||
"@types/mustache": "^0.8.31",
|
||||
"JSONStream": "1.1.1",
|
||||
"abortcontroller-polyfill": "^1.1.9",
|
||||
"angular": "1.6.9",
|
||||
"angular-aria": "1.6.6",
|
||||
|
|
|
@ -63,19 +63,22 @@ function checkout_sibling {
|
|||
|
||||
cloneAuthor="elastic"
|
||||
cloneBranch="${PR_SOURCE_BRANCH:-${GIT_BRANCH#*/}}" # GIT_BRANCH starts with the repo, i.e., origin/master
|
||||
cloneBranch="${cloneBranch:-master}" # fall back to CI branch if not testing a PR
|
||||
if clone_target_is_valid ; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
cloneBranch="$PR_TARGET_BRANCH"
|
||||
cloneBranch="${PR_TARGET_BRANCH:-master}"
|
||||
if clone_target_is_valid ; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# remove xpack_ prefix from target branch if all other options fail
|
||||
cloneBranch="${PR_TARGET_BRANCH#xpack_}"
|
||||
return 0
|
||||
cloneBranch="master"
|
||||
if clone_target_is_valid; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "failed to find a valid branch to clone"
|
||||
return 1
|
||||
}
|
||||
|
||||
function checkout_clone_target {
|
||||
|
|
22
src/type_definitions/react_virtualized.d.ts
vendored
Normal file
22
src/type_definitions/react_virtualized.d.ts
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module 'react-virtualized' {
|
||||
export type ListProps = any;
|
||||
}
|
55
src/ui/public/autocomplete_providers/index.d.ts
vendored
Normal file
55
src/ui/public/autocomplete_providers/index.d.ts
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* WARNING: these typings are incomplete
|
||||
*/
|
||||
import { StaticIndexPattern } from 'ui/index_patterns';
|
||||
|
||||
export type AutocompleteProvider = (
|
||||
args: {
|
||||
config: {
|
||||
get(configKey: string): any;
|
||||
};
|
||||
indexPatterns: StaticIndexPattern[];
|
||||
boolFilter: any;
|
||||
}
|
||||
) => GetSuggestions;
|
||||
|
||||
export type GetSuggestions = (
|
||||
args: {
|
||||
query: string;
|
||||
selectionStart: number;
|
||||
selectionEnd: number;
|
||||
}
|
||||
) => Promise<AutocompleteSuggestion[]>;
|
||||
|
||||
export type AutocompleteSuggestionType = 'field' | 'value' | 'operator' | 'conjunction';
|
||||
|
||||
export interface AutocompleteSuggestion {
|
||||
description: string;
|
||||
end: number;
|
||||
start: number;
|
||||
text: string;
|
||||
type: AutocompleteSuggestionType;
|
||||
}
|
||||
|
||||
export function addAutocompleteProvider(language: string, provider: AutocompleteProvider): void;
|
||||
|
||||
export function getAutocompleteProvider(language: string): AutocompleteProvider | undefined;
|
16
src/ui/public/index_patterns/_index_pattern.d.ts
vendored
16
src/ui/public/index_patterns/_index_pattern.d.ts
vendored
|
@ -17,4 +17,20 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* WARNING: these types are incomplete
|
||||
*/
|
||||
|
||||
export type IndexPattern = any;
|
||||
|
||||
export interface StaticIndexPatternField {
|
||||
name: string;
|
||||
type: string;
|
||||
aggregatable: boolean;
|
||||
searchable: boolean;
|
||||
}
|
||||
|
||||
export interface StaticIndexPattern {
|
||||
fields: StaticIndexPatternField[];
|
||||
title: string;
|
||||
}
|
||||
|
|
2
src/ui/public/index_patterns/index.d.ts
vendored
2
src/ui/public/index_patterns/index.d.ts
vendored
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { IndexPattern } from './_index_pattern';
|
||||
export { IndexPattern, StaticIndexPattern } from 'ui/index_patterns/_index_pattern';
|
||||
|
|
48
src/ui/public/kuery/ast/ast.d.ts
vendored
Normal file
48
src/ui/public/kuery/ast/ast.d.ts
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* WARNING: these typings are incomplete
|
||||
*/
|
||||
|
||||
import { StaticIndexPattern } from 'ui/index_patterns';
|
||||
|
||||
export type KueryNode = any;
|
||||
|
||||
export interface KueryParseOptions {
|
||||
helpers: {
|
||||
[key: string]: any;
|
||||
};
|
||||
startRule: string;
|
||||
}
|
||||
|
||||
type JsonValue = null | boolean | number | string | JsonObject | JsonArray;
|
||||
|
||||
interface JsonObject {
|
||||
[key: string]: JsonValue;
|
||||
}
|
||||
|
||||
interface JsonArray extends Array<JsonValue> {}
|
||||
|
||||
export function fromKueryExpression(
|
||||
expression: string,
|
||||
parseOptions?: KueryParseOptions
|
||||
): KueryNode;
|
||||
|
||||
export function toElasticsearchQuery(node: KueryNode, indexPattern: StaticIndexPattern): JsonObject;
|
20
src/ui/public/kuery/ast/index.d.ts
vendored
Normal file
20
src/ui/public/kuery/ast/index.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from 'ui/kuery/ast/ast';
|
20
src/ui/public/kuery/index.d.ts
vendored
Normal file
20
src/ui/public/kuery/index.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from 'ui/kuery/ast';
|
40
src/ui/public/registry/feature_catalogue.d.ts
vendored
Normal file
40
src/ui/public/registry/feature_catalogue.d.ts
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export enum FeatureCatalogueCategory {
|
||||
ADMIN = 'admin',
|
||||
DATA = 'data',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
interface FeatureCatalogueObject {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
path: string;
|
||||
showOnHomePage: boolean;
|
||||
category: FeatureCatalogueCategory;
|
||||
}
|
||||
|
||||
type FeatureCatalogueRegistryFunction = () => FeatureCatalogueObject;
|
||||
|
||||
export const FeatureCatalogueRegistryProvider: {
|
||||
register: (fn: FeatureCatalogueRegistryFunction) => void;
|
||||
};
|
23
src/ui/public/routes/index.d.ts
vendored
Normal file
23
src/ui/public/routes/index.d.ts
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { uiRoutes, UIRoutes } from 'ui/routes/routes';
|
||||
|
||||
export default uiRoutes;
|
||||
export { UIRoutes };
|
38
src/ui/public/routes/route_manager.d.ts
vendored
Normal file
38
src/ui/public/routes/route_manager.d.ts
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* WARNING: these types are incomplete
|
||||
*/
|
||||
|
||||
interface RouteConfiguration {
|
||||
controller?: string | ((...args: any[]) => void);
|
||||
redirectTo?: string;
|
||||
reloadOnSearch?: boolean;
|
||||
resolve?: object;
|
||||
template?: string;
|
||||
}
|
||||
|
||||
interface RouteManager {
|
||||
when(path: string, routeConfiguration: RouteConfiguration): RouteManager;
|
||||
otherwise(routeConfiguration: RouteConfiguration): RouteManager;
|
||||
defaults(path: string | RegExp, defaults: RouteConfiguration): RouteManager;
|
||||
}
|
||||
|
||||
export default RouteManager;
|
27
src/ui/public/routes/routes.d.ts
vendored
Normal file
27
src/ui/public/routes/routes.d.ts
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import RouteManager from 'ui/routes/route_manager';
|
||||
|
||||
interface DefaultRouteManager extends RouteManager {
|
||||
enable(): void;
|
||||
}
|
||||
|
||||
export const uiRoutes: DefaultRouteManager;
|
||||
export type UIRoutes = DefaultRouteManager;
|
1
x-pack/.gitignore
vendored
1
x-pack/.gitignore
vendored
|
@ -9,3 +9,4 @@
|
|||
/.aws-config.json
|
||||
/.env
|
||||
/.kibana-plugin-helpers.dev.*
|
||||
!/plugins/infra/**/target
|
||||
|
|
|
@ -25,6 +25,7 @@ import { spaces } from './plugins/spaces';
|
|||
import { notifications } from './plugins/notifications';
|
||||
import { kueryAutocomplete } from './plugins/kuery_autocomplete';
|
||||
import { canvas } from './plugins/canvas';
|
||||
import { infra } from './plugins/infra';
|
||||
|
||||
module.exports = function (kibana) {
|
||||
return [
|
||||
|
@ -48,6 +49,7 @@ module.exports = function (kibana) {
|
|||
indexManagement(kibana),
|
||||
consoleExtensions(kibana),
|
||||
notifications(kibana),
|
||||
kueryAutocomplete(kibana)
|
||||
kueryAutocomplete(kibana),
|
||||
infra(kibana),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -25,12 +25,32 @@
|
|||
"@kbn/es": "link:../packages/kbn-es",
|
||||
"@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers",
|
||||
"@kbn/test": "link:../packages/kbn-test",
|
||||
"@types/angular": "^1.6.50",
|
||||
"@types/d3-array": "^1.2.1",
|
||||
"@types/d3-scale": "^2.0.0",
|
||||
"@types/d3-shape": "^1.2.2",
|
||||
"@types/d3-time": "^1.0.7",
|
||||
"@types/d3-time-format": "^2.1.0",
|
||||
"@types/elasticsearch": "^5.0.22",
|
||||
"@types/expect.js": "^0.3.29",
|
||||
"@types/graphql": "^0.13.1",
|
||||
"@types/hapi": "15.0.1",
|
||||
"@types/history": "^4.6.2",
|
||||
"@types/jest": "^23.3.1",
|
||||
"@types/joi": "^10.4.4",
|
||||
"@types/lodash": "^3.10.1",
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/pngjs": "^3.3.1",
|
||||
"@types/prop-types": "^15.5.3",
|
||||
"@types/react": "^16.3.14",
|
||||
"@types/react-datepicker": "^1.1.5",
|
||||
"@types/react-dom": "^16.0.5",
|
||||
"@types/react-redux": "^6.0.6",
|
||||
"@types/react-router-dom": "^4.2.6",
|
||||
"@types/reduce-reducers": "^0.1.3",
|
||||
"@types/sinon": "^5.0.1",
|
||||
"@types/supertest": "^2.0.5",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"abab": "^1.0.4",
|
||||
"ansi-colors": "^3.0.5",
|
||||
"ansicolors": "0.3.2",
|
||||
|
@ -55,6 +75,9 @@
|
|||
"expect.js": "0.3.1",
|
||||
"fancy-log": "^1.3.2",
|
||||
"fetch-mock": "^5.13.1",
|
||||
"graphql-code-generator": "^0.10.1",
|
||||
"graphql-codegen-introspection-template": "^0.10.5",
|
||||
"graphql-codegen-typescript-template": "^0.10.1",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-mocha": "2.2.0",
|
||||
"gulp-multi-process": "^1.3.1",
|
||||
|
@ -105,6 +128,14 @@
|
|||
"angular-resource": "1.4.9",
|
||||
"angular-sanitize": "1.4.9",
|
||||
"angular-ui-ace": "0.2.3",
|
||||
"apollo-cache-inmemory": "^1.2.7",
|
||||
"apollo-client": "^2.3.8",
|
||||
"apollo-link": "^1.2.2",
|
||||
"apollo-link-http": "^1.5.4",
|
||||
"apollo-link-schema": "^1.1.0",
|
||||
"apollo-link-state": "^0.4.1",
|
||||
"apollo-server-errors": "^2.0.2",
|
||||
"apollo-server-hapi": "^1.3.6",
|
||||
"axios": "^0.18.0",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
|
@ -119,6 +150,7 @@
|
|||
"copy-to-clipboard": "^3.0.8",
|
||||
"d3": "3.5.6",
|
||||
"d3-scale": "1.0.6",
|
||||
"dataloader": "^1.4.0",
|
||||
"dedent": "^0.7.0",
|
||||
"dragselect": "1.8.1",
|
||||
"elasticsearch": "^15.1.1",
|
||||
|
@ -128,6 +160,10 @@
|
|||
"get-port": "2.1.0",
|
||||
"getos": "^3.1.0",
|
||||
"glob": "6.0.4",
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-fields": "^1.0.2",
|
||||
"graphql-tag": "^2.9.2",
|
||||
"graphql-tools": "^3.0.2",
|
||||
"handlebars": "^4.0.10",
|
||||
"hapi-auth-cookie": "6.1.1",
|
||||
"history": "4.7.2",
|
||||
|
@ -169,6 +205,7 @@
|
|||
"puppeteer-core": "^1.7.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"react": "^16.3.0",
|
||||
"react-apollo": "^2.1.4",
|
||||
"react-beautiful-dnd": "^8.0.7",
|
||||
"react-clipboard.js": "^1.1.2",
|
||||
"react-datetime": "^2.14.0",
|
||||
|
@ -186,13 +223,15 @@
|
|||
"react-syntax-highlighter": "^5.7.0",
|
||||
"react-vis": "^1.8.1",
|
||||
"recompose": "^0.26.0",
|
||||
"reduce-reducers": "^0.1.2",
|
||||
"reduce-reducers": "^0.4.3",
|
||||
"redux": "4.0.0",
|
||||
"redux-actions": "2.2.1",
|
||||
"redux-observable": "^1.0.0",
|
||||
"redux-thunk": "2.3.0",
|
||||
"redux-thunks": "^1.0.0",
|
||||
"request": "^2.85.0",
|
||||
"reselect": "3.0.1",
|
||||
"resize-observer-polyfill": "^1.5.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"rison-node": "0.3.1",
|
||||
"rxjs": "^6.2.1",
|
||||
|
@ -208,6 +247,8 @@
|
|||
"tinycolor2": "1.3.0",
|
||||
"tinymath": "^0.5.0",
|
||||
"tslib": "^1.9.3",
|
||||
"typescript-fsa": "^2.5.0",
|
||||
"typescript-fsa-reducers": "^0.4.5",
|
||||
"ui-select": "0.19.4",
|
||||
"unbzip2-stream": "1.0.9",
|
||||
"uuid": "3.0.1",
|
||||
|
|
2666
x-pack/plugins/infra/common/graphql/introspection.json
Normal file
2666
x-pack/plugins/infra/common/graphql/introspection.json
Normal file
File diff suppressed because it is too large
Load diff
7
x-pack/plugins/infra/common/graphql/root/index.ts
Normal file
7
x-pack/plugins/infra/common/graphql/root/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { rootSchema } from './schema.gql';
|
18
x-pack/plugins/infra/common/graphql/root/schema.gql.ts
Normal file
18
x-pack/plugins/infra/common/graphql/root/schema.gql.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const rootSchema = gql`
|
||||
schema {
|
||||
query: Query
|
||||
#mutation: Mutation
|
||||
}
|
||||
|
||||
type Query
|
||||
|
||||
#type Mutation
|
||||
`;
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const sharedFragments = {
|
||||
InfraTimeKey: gql`
|
||||
fragment InfraTimeKeyFields on InfraTimeKey {
|
||||
time
|
||||
tiebreaker
|
||||
}
|
||||
`,
|
||||
};
|
8
x-pack/plugins/infra/common/graphql/shared/index.ts
Normal file
8
x-pack/plugins/infra/common/graphql/shared/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { sharedFragments } from './fragments.gql_query';
|
||||
export { sharedSchema } from './schema.gql';
|
34
x-pack/plugins/infra/common/graphql/shared/schema.gql.ts
Normal file
34
x-pack/plugins/infra/common/graphql/shared/schema.gql.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const sharedSchema = gql`
|
||||
"A representation of the log entry's position in the event stream"
|
||||
type InfraTimeKey {
|
||||
"The timestamp of the event that the log entry corresponds to"
|
||||
time: Float!
|
||||
"The tiebreaker that disambiguates events with the same timestamp"
|
||||
tiebreaker: Float!
|
||||
}
|
||||
|
||||
input InfraTimeKeyInput {
|
||||
time: Float!
|
||||
tiebreaker: Float!
|
||||
}
|
||||
|
||||
enum InfraIndexType {
|
||||
ANY
|
||||
LOGS
|
||||
METRICS
|
||||
}
|
||||
|
||||
enum InfraNodeType {
|
||||
pod
|
||||
container
|
||||
host
|
||||
}
|
||||
`;
|
82
x-pack/plugins/infra/common/graphql/typed_resolvers.ts
Normal file
82
x-pack/plugins/infra/common/graphql/typed_resolvers.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
type BasicResolver<Result, Args = any> = (
|
||||
parent: any,
|
||||
args: Args,
|
||||
context: any,
|
||||
info: GraphQLResolveInfo
|
||||
) => Promise<Result> | Result;
|
||||
|
||||
type InfraResolverResult<R> =
|
||||
| Promise<R>
|
||||
| Promise<{ [P in keyof R]: () => Promise<R[P]> }>
|
||||
| { [P in keyof R]: () => Promise<R[P]> }
|
||||
| { [P in keyof R]: () => R[P] }
|
||||
| R;
|
||||
|
||||
export type InfraResolvedResult<Resolver> = Resolver extends InfraResolver<
|
||||
infer Result,
|
||||
any,
|
||||
any,
|
||||
any
|
||||
>
|
||||
? Result
|
||||
: never;
|
||||
|
||||
export type SubsetResolverWithFields<R, IncludedFields extends string> = R extends BasicResolver<
|
||||
Array<infer ResultInArray>,
|
||||
infer ArgsInArray
|
||||
>
|
||||
? BasicResolver<
|
||||
Array<Pick<ResultInArray, Extract<keyof ResultInArray, IncludedFields>>>,
|
||||
ArgsInArray
|
||||
>
|
||||
: R extends BasicResolver<infer Result, infer Args>
|
||||
? BasicResolver<Pick<Result, Extract<keyof Result, IncludedFields>>, Args>
|
||||
: never;
|
||||
|
||||
export type SubsetResolverWithoutFields<R, ExcludedFields extends string> = R extends BasicResolver<
|
||||
Array<infer ResultInArray>,
|
||||
infer ArgsInArray
|
||||
>
|
||||
? BasicResolver<
|
||||
Array<Pick<ResultInArray, Exclude<keyof ResultInArray, ExcludedFields>>>,
|
||||
ArgsInArray
|
||||
>
|
||||
: R extends BasicResolver<infer Result, infer Args>
|
||||
? BasicResolver<Pick<Result, Exclude<keyof Result, ExcludedFields>>, Args>
|
||||
: never;
|
||||
|
||||
export type InfraResolver<Result, Parent, Args, Context> = (
|
||||
parent: Parent,
|
||||
args: Args,
|
||||
context: Context,
|
||||
info: GraphQLResolveInfo
|
||||
) => InfraResolverResult<Result>;
|
||||
|
||||
export type InfraResolverOf<Resolver, Parent, Context> = Resolver extends BasicResolver<
|
||||
infer Result,
|
||||
infer Args
|
||||
>
|
||||
? InfraResolver<Result, Parent, Args, Context>
|
||||
: never;
|
||||
|
||||
export type InfraResolverWithFields<
|
||||
Resolver,
|
||||
Parent,
|
||||
Context,
|
||||
IncludedFields extends string
|
||||
> = InfraResolverOf<SubsetResolverWithFields<Resolver, IncludedFields>, Parent, Context>;
|
||||
|
||||
export type InfraResolverWithoutFields<
|
||||
Resolver,
|
||||
Parent,
|
||||
Context,
|
||||
ExcludedFields extends string
|
||||
> = InfraResolverOf<SubsetResolverWithoutFields<Resolver, ExcludedFields>, Parent, Context>;
|
858
x-pack/plugins/infra/common/graphql/types.ts
Normal file
858
x-pack/plugins/infra/common/graphql/types.ts
Normal file
|
@ -0,0 +1,858 @@
|
|||
/* tslint:disable */
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
type Resolver<Result, Args = any> = (
|
||||
parent: any,
|
||||
args: Args,
|
||||
context: any,
|
||||
info: GraphQLResolveInfo
|
||||
) => Promise<Result> | Result;
|
||||
|
||||
export interface Query {
|
||||
source: InfraSource /** Get an infrastructure data source by id */;
|
||||
allSources: InfraSource[] /** Get a list of all infrastructure data sources */;
|
||||
}
|
||||
/** A source of infrastructure data */
|
||||
export interface InfraSource {
|
||||
id: string /** The id of the source */;
|
||||
configuration: InfraSourceConfiguration /** The raw configuration of the source */;
|
||||
status: InfraSourceStatus /** The status of the source */;
|
||||
capabilitiesByNode: (InfraNodeCapability | null)[] /** A hierarchy of capabilities available on nodes */;
|
||||
logEntriesAround: InfraLogEntryInterval /** A consecutive span of log entries surrounding a point in time */;
|
||||
logEntriesBetween: InfraLogEntryInterval /** A consecutive span of log entries within an interval */;
|
||||
logSummaryBetween: InfraLogSummaryInterval /** A consecutive span of summary buckets within an interval */;
|
||||
map?: InfraResponse | null /** A hierarchy of hosts, pods, containers, services or arbitrary groups */;
|
||||
metrics: InfraMetricData[];
|
||||
}
|
||||
/** A set of configuration options for an infrastructure data source */
|
||||
export interface InfraSourceConfiguration {
|
||||
metricAlias: string /** The alias to read metric data from */;
|
||||
logAlias: string /** The alias to read log data from */;
|
||||
fields: InfraSourceFields /** The field mapping to use for this source */;
|
||||
}
|
||||
/** A mapping of semantic fields to their document counterparts */
|
||||
export interface InfraSourceFields {
|
||||
container: string /** The field to identify a container by */;
|
||||
host: string /** The fields to identify a host by */;
|
||||
message: string[] /** The fields that may contain the log event message. The first field found win. */;
|
||||
pod: string /** The field to identify a pod by */;
|
||||
tiebreaker: string /** The field to use as a tiebreaker for log events that have identical timestamps */;
|
||||
timestamp: string /** The field to use as a timestamp for metrics and logs */;
|
||||
}
|
||||
/** The status of an infrastructure data source */
|
||||
export interface InfraSourceStatus {
|
||||
metricAliasExists: boolean /** Whether the configured metric alias exists */;
|
||||
logAliasExists: boolean /** Whether the configured log alias exists */;
|
||||
metricIndicesExist: boolean /** Whether the configured alias or wildcard pattern resolve to any metric indices */;
|
||||
logIndicesExist: boolean /** Whether the configured alias or wildcard pattern resolve to any log indices */;
|
||||
metricIndices: string[] /** The list of indices in the metric alias */;
|
||||
logIndices: string[] /** The list of indices in the log alias */;
|
||||
indexFields: InfraIndexField[] /** The list of fields defined in the index mappings */;
|
||||
}
|
||||
/** A descriptor of a field in an index */
|
||||
export interface InfraIndexField {
|
||||
name: string /** The name of the field */;
|
||||
type: string /** The type of the field's values as recognized by Kibana */;
|
||||
searchable: boolean /** Whether the field's values can be efficiently searched for */;
|
||||
aggregatable: boolean /** Whether the field's values can be aggregated */;
|
||||
}
|
||||
/** One specific capability available on a node. A capability corresponds to a fileset or metricset */
|
||||
export interface InfraNodeCapability {
|
||||
name: string;
|
||||
source: string;
|
||||
}
|
||||
/** A consecutive sequence of log entries */
|
||||
export interface InfraLogEntryInterval {
|
||||
start?: InfraTimeKey | null /** The key corresponding to the start of the interval covered by the entries */;
|
||||
end?: InfraTimeKey | null /** The key corresponding to the end of the interval covered by the entries */;
|
||||
hasMoreBefore: boolean /** Whether there are more log entries available before the start */;
|
||||
hasMoreAfter: boolean /** Whether there are more log entries available after the end */;
|
||||
filterQuery?: string | null /** The query the log entries were filtered by */;
|
||||
highlightQuery?: string | null /** The query the log entries were highlighted with */;
|
||||
entries: InfraLogEntry[] /** A list of the log entries */;
|
||||
}
|
||||
/** A representation of the log entry's position in the event stream */
|
||||
export interface InfraTimeKey {
|
||||
time: number /** The timestamp of the event that the log entry corresponds to */;
|
||||
tiebreaker: number /** The tiebreaker that disambiguates events with the same timestamp */;
|
||||
}
|
||||
/** A log entry */
|
||||
export interface InfraLogEntry {
|
||||
key: InfraTimeKey /** A unique representation of the log entry's position in the event stream */;
|
||||
gid: string /** The log entry's id */;
|
||||
source: string /** The source id */;
|
||||
message: InfraLogMessageSegment[] /** A list of the formatted log entry segments */;
|
||||
}
|
||||
/** A segment of the log entry message that was derived from a field */
|
||||
export interface InfraLogMessageFieldSegment {
|
||||
field: string /** The field the segment was derived from */;
|
||||
value: string /** The segment's message */;
|
||||
highlights: string[] /** A list of highlighted substrings of the value */;
|
||||
}
|
||||
/** A segment of the log entry message that was derived from a field */
|
||||
export interface InfraLogMessageConstantSegment {
|
||||
constant: string /** The segment's message */;
|
||||
}
|
||||
/** A consecutive sequence of log summary buckets */
|
||||
export interface InfraLogSummaryInterval {
|
||||
start?:
|
||||
| number
|
||||
| null /** The millisecond timestamp corresponding to the start of the interval covered by the summary */;
|
||||
end?:
|
||||
| number
|
||||
| null /** The millisecond timestamp corresponding to the end of the interval covered by the summary */;
|
||||
filterQuery?: string | null /** The query the log entries were filtered by */;
|
||||
buckets: InfraLogSummaryBucket[] /** A list of the log entries */;
|
||||
}
|
||||
/** A log summary bucket */
|
||||
export interface InfraLogSummaryBucket {
|
||||
start: number /** The start timestamp of the bucket */;
|
||||
end: number /** The end timestamp of the bucket */;
|
||||
entriesCount: number /** The number of entries inside the bucket */;
|
||||
}
|
||||
|
||||
export interface InfraResponse {
|
||||
nodes: InfraNode[];
|
||||
}
|
||||
|
||||
export interface InfraNode {
|
||||
path: InfraNodePath[];
|
||||
metric: InfraNodeMetric;
|
||||
}
|
||||
|
||||
export interface InfraNodePath {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface InfraNodeMetric {
|
||||
name: InfraMetricType;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface InfraMetricData {
|
||||
id?: InfraMetric | null;
|
||||
series: InfraDataSeries[];
|
||||
}
|
||||
|
||||
export interface InfraDataSeries {
|
||||
id: string;
|
||||
data: InfraDataPoint[];
|
||||
}
|
||||
|
||||
export interface InfraDataPoint {
|
||||
timestamp: number;
|
||||
value?: number | null;
|
||||
}
|
||||
|
||||
export namespace QueryResolvers {
|
||||
export interface Resolvers {
|
||||
source?: SourceResolver /** Get an infrastructure data source by id */;
|
||||
allSources?: AllSourcesResolver /** Get a list of all infrastructure data sources */;
|
||||
}
|
||||
|
||||
export type SourceResolver = Resolver<InfraSource, SourceArgs>;
|
||||
export interface SourceArgs {
|
||||
id: string /** The id of the source */;
|
||||
}
|
||||
|
||||
export type AllSourcesResolver = Resolver<InfraSource[]>;
|
||||
}
|
||||
/** A source of infrastructure data */
|
||||
export namespace InfraSourceResolvers {
|
||||
export interface Resolvers {
|
||||
id?: IdResolver /** The id of the source */;
|
||||
configuration?: ConfigurationResolver /** The raw configuration of the source */;
|
||||
status?: StatusResolver /** The status of the source */;
|
||||
capabilitiesByNode?: CapabilitiesByNodeResolver /** A hierarchy of capabilities available on nodes */;
|
||||
logEntriesAround?: LogEntriesAroundResolver /** A consecutive span of log entries surrounding a point in time */;
|
||||
logEntriesBetween?: LogEntriesBetweenResolver /** A consecutive span of log entries within an interval */;
|
||||
logSummaryBetween?: LogSummaryBetweenResolver /** A consecutive span of summary buckets within an interval */;
|
||||
map?: MapResolver /** A hierarchy of hosts, pods, containers, services or arbitrary groups */;
|
||||
metrics?: MetricsResolver;
|
||||
}
|
||||
|
||||
export type IdResolver = Resolver<string>;
|
||||
export type ConfigurationResolver = Resolver<InfraSourceConfiguration>;
|
||||
export type StatusResolver = Resolver<InfraSourceStatus>;
|
||||
export type CapabilitiesByNodeResolver = Resolver<
|
||||
(InfraNodeCapability | null)[],
|
||||
CapabilitiesByNodeArgs
|
||||
>;
|
||||
export interface CapabilitiesByNodeArgs {
|
||||
nodeName: string;
|
||||
nodeType: InfraNodeType;
|
||||
}
|
||||
|
||||
export type LogEntriesAroundResolver = Resolver<InfraLogEntryInterval, LogEntriesAroundArgs>;
|
||||
export interface LogEntriesAroundArgs {
|
||||
key: InfraTimeKeyInput /** The sort key that corresponds to the point in time */;
|
||||
countBefore?: number | null /** The maximum number of preceding to return */;
|
||||
countAfter?: number | null /** The maximum number of following to return */;
|
||||
filterQuery?: string | null /** The query to filter the log entries by */;
|
||||
highlightQuery?: string | null /** The query to highlight the log entries with */;
|
||||
}
|
||||
|
||||
export type LogEntriesBetweenResolver = Resolver<InfraLogEntryInterval, LogEntriesBetweenArgs>;
|
||||
export interface LogEntriesBetweenArgs {
|
||||
startKey: InfraTimeKeyInput /** The sort key that corresponds to the start of the interval */;
|
||||
endKey: InfraTimeKeyInput /** The sort key that corresponds to the end of the interval */;
|
||||
filterQuery?: string | null /** The query to filter the log entries by */;
|
||||
highlightQuery?: string | null /** The query to highlight the log entries with */;
|
||||
}
|
||||
|
||||
export type LogSummaryBetweenResolver = Resolver<InfraLogSummaryInterval, LogSummaryBetweenArgs>;
|
||||
export interface LogSummaryBetweenArgs {
|
||||
start: number /** The millisecond timestamp that corresponds to the start of the interval */;
|
||||
end: number /** The millisecond timestamp that corresponds to the end of the interval */;
|
||||
bucketSize: number /** The size of each bucket in milliseconds */;
|
||||
filterQuery?: string | null /** The query to filter the log entries by */;
|
||||
}
|
||||
|
||||
export type MapResolver = Resolver<InfraResponse | null, MapArgs>;
|
||||
export interface MapArgs {
|
||||
timerange: InfraTimerangeInput;
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
|
||||
export type MetricsResolver = Resolver<InfraMetricData[], MetricsArgs>;
|
||||
export interface MetricsArgs {
|
||||
nodeId: string;
|
||||
nodeType: InfraNodeType;
|
||||
timerange: InfraTimerangeInput;
|
||||
metrics: InfraMetric[];
|
||||
}
|
||||
}
|
||||
/** A set of configuration options for an infrastructure data source */
|
||||
export namespace InfraSourceConfigurationResolvers {
|
||||
export interface Resolvers {
|
||||
metricAlias?: MetricAliasResolver /** The alias to read metric data from */;
|
||||
logAlias?: LogAliasResolver /** The alias to read log data from */;
|
||||
fields?: FieldsResolver /** The field mapping to use for this source */;
|
||||
}
|
||||
|
||||
export type MetricAliasResolver = Resolver<string>;
|
||||
export type LogAliasResolver = Resolver<string>;
|
||||
export type FieldsResolver = Resolver<InfraSourceFields>;
|
||||
}
|
||||
/** A mapping of semantic fields to their document counterparts */
|
||||
export namespace InfraSourceFieldsResolvers {
|
||||
export interface Resolvers {
|
||||
container?: ContainerResolver /** The field to identify a container by */;
|
||||
host?: HostResolver /** The fields to identify a host by */;
|
||||
message?: MessageResolver /** The fields that may contain the log event message. The first field found win. */;
|
||||
pod?: PodResolver /** The field to identify a pod by */;
|
||||
tiebreaker?: TiebreakerResolver /** The field to use as a tiebreaker for log events that have identical timestamps */;
|
||||
timestamp?: TimestampResolver /** The field to use as a timestamp for metrics and logs */;
|
||||
}
|
||||
|
||||
export type ContainerResolver = Resolver<string>;
|
||||
export type HostResolver = Resolver<string>;
|
||||
export type MessageResolver = Resolver<string[]>;
|
||||
export type PodResolver = Resolver<string>;
|
||||
export type TiebreakerResolver = Resolver<string>;
|
||||
export type TimestampResolver = Resolver<string>;
|
||||
}
|
||||
/** The status of an infrastructure data source */
|
||||
export namespace InfraSourceStatusResolvers {
|
||||
export interface Resolvers {
|
||||
metricAliasExists?: MetricAliasExistsResolver /** Whether the configured metric alias exists */;
|
||||
logAliasExists?: LogAliasExistsResolver /** Whether the configured log alias exists */;
|
||||
metricIndicesExist?: MetricIndicesExistResolver /** Whether the configured alias or wildcard pattern resolve to any metric indices */;
|
||||
logIndicesExist?: LogIndicesExistResolver /** Whether the configured alias or wildcard pattern resolve to any log indices */;
|
||||
metricIndices?: MetricIndicesResolver /** The list of indices in the metric alias */;
|
||||
logIndices?: LogIndicesResolver /** The list of indices in the log alias */;
|
||||
indexFields?: IndexFieldsResolver /** The list of fields defined in the index mappings */;
|
||||
}
|
||||
|
||||
export type MetricAliasExistsResolver = Resolver<boolean>;
|
||||
export type LogAliasExistsResolver = Resolver<boolean>;
|
||||
export type MetricIndicesExistResolver = Resolver<boolean>;
|
||||
export type LogIndicesExistResolver = Resolver<boolean>;
|
||||
export type MetricIndicesResolver = Resolver<string[]>;
|
||||
export type LogIndicesResolver = Resolver<string[]>;
|
||||
export type IndexFieldsResolver = Resolver<InfraIndexField[], IndexFieldsArgs>;
|
||||
export interface IndexFieldsArgs {
|
||||
indexType?: InfraIndexType | null;
|
||||
}
|
||||
}
|
||||
/** A descriptor of a field in an index */
|
||||
export namespace InfraIndexFieldResolvers {
|
||||
export interface Resolvers {
|
||||
name?: NameResolver /** The name of the field */;
|
||||
type?: TypeResolver /** The type of the field's values as recognized by Kibana */;
|
||||
searchable?: SearchableResolver /** Whether the field's values can be efficiently searched for */;
|
||||
aggregatable?: AggregatableResolver /** Whether the field's values can be aggregated */;
|
||||
}
|
||||
|
||||
export type NameResolver = Resolver<string>;
|
||||
export type TypeResolver = Resolver<string>;
|
||||
export type SearchableResolver = Resolver<boolean>;
|
||||
export type AggregatableResolver = Resolver<boolean>;
|
||||
}
|
||||
/** One specific capability available on a node. A capability corresponds to a fileset or metricset */
|
||||
export namespace InfraNodeCapabilityResolvers {
|
||||
export interface Resolvers {
|
||||
name?: NameResolver;
|
||||
source?: SourceResolver;
|
||||
}
|
||||
|
||||
export type NameResolver = Resolver<string>;
|
||||
export type SourceResolver = Resolver<string>;
|
||||
}
|
||||
/** A consecutive sequence of log entries */
|
||||
export namespace InfraLogEntryIntervalResolvers {
|
||||
export interface Resolvers {
|
||||
start?: StartResolver /** The key corresponding to the start of the interval covered by the entries */;
|
||||
end?: EndResolver /** The key corresponding to the end of the interval covered by the entries */;
|
||||
hasMoreBefore?: HasMoreBeforeResolver /** Whether there are more log entries available before the start */;
|
||||
hasMoreAfter?: HasMoreAfterResolver /** Whether there are more log entries available after the end */;
|
||||
filterQuery?: FilterQueryResolver /** The query the log entries were filtered by */;
|
||||
highlightQuery?: HighlightQueryResolver /** The query the log entries were highlighted with */;
|
||||
entries?: EntriesResolver /** A list of the log entries */;
|
||||
}
|
||||
|
||||
export type StartResolver = Resolver<InfraTimeKey | null>;
|
||||
export type EndResolver = Resolver<InfraTimeKey | null>;
|
||||
export type HasMoreBeforeResolver = Resolver<boolean>;
|
||||
export type HasMoreAfterResolver = Resolver<boolean>;
|
||||
export type FilterQueryResolver = Resolver<string | null>;
|
||||
export type HighlightQueryResolver = Resolver<string | null>;
|
||||
export type EntriesResolver = Resolver<InfraLogEntry[]>;
|
||||
}
|
||||
/** A representation of the log entry's position in the event stream */
|
||||
export namespace InfraTimeKeyResolvers {
|
||||
export interface Resolvers {
|
||||
time?: TimeResolver /** The timestamp of the event that the log entry corresponds to */;
|
||||
tiebreaker?: TiebreakerResolver /** The tiebreaker that disambiguates events with the same timestamp */;
|
||||
}
|
||||
|
||||
export type TimeResolver = Resolver<number>;
|
||||
export type TiebreakerResolver = Resolver<number>;
|
||||
}
|
||||
/** A log entry */
|
||||
export namespace InfraLogEntryResolvers {
|
||||
export interface Resolvers {
|
||||
key?: KeyResolver /** A unique representation of the log entry's position in the event stream */;
|
||||
gid?: GidResolver /** The log entry's id */;
|
||||
source?: SourceResolver /** The source id */;
|
||||
message?: MessageResolver /** A list of the formatted log entry segments */;
|
||||
}
|
||||
|
||||
export type KeyResolver = Resolver<InfraTimeKey>;
|
||||
export type GidResolver = Resolver<string>;
|
||||
export type SourceResolver = Resolver<string>;
|
||||
export type MessageResolver = Resolver<InfraLogMessageSegment[]>;
|
||||
}
|
||||
/** A segment of the log entry message that was derived from a field */
|
||||
export namespace InfraLogMessageFieldSegmentResolvers {
|
||||
export interface Resolvers {
|
||||
field?: FieldResolver /** The field the segment was derived from */;
|
||||
value?: ValueResolver /** The segment's message */;
|
||||
highlights?: HighlightsResolver /** A list of highlighted substrings of the value */;
|
||||
}
|
||||
|
||||
export type FieldResolver = Resolver<string>;
|
||||
export type ValueResolver = Resolver<string>;
|
||||
export type HighlightsResolver = Resolver<string[]>;
|
||||
}
|
||||
/** A segment of the log entry message that was derived from a field */
|
||||
export namespace InfraLogMessageConstantSegmentResolvers {
|
||||
export interface Resolvers {
|
||||
constant?: ConstantResolver /** The segment's message */;
|
||||
}
|
||||
|
||||
export type ConstantResolver = Resolver<string>;
|
||||
}
|
||||
/** A consecutive sequence of log summary buckets */
|
||||
export namespace InfraLogSummaryIntervalResolvers {
|
||||
export interface Resolvers {
|
||||
start?: StartResolver /** The millisecond timestamp corresponding to the start of the interval covered by the summary */;
|
||||
end?: EndResolver /** The millisecond timestamp corresponding to the end of the interval covered by the summary */;
|
||||
filterQuery?: FilterQueryResolver /** The query the log entries were filtered by */;
|
||||
buckets?: BucketsResolver /** A list of the log entries */;
|
||||
}
|
||||
|
||||
export type StartResolver = Resolver<number | null>;
|
||||
export type EndResolver = Resolver<number | null>;
|
||||
export type FilterQueryResolver = Resolver<string | null>;
|
||||
export type BucketsResolver = Resolver<InfraLogSummaryBucket[]>;
|
||||
}
|
||||
/** A log summary bucket */
|
||||
export namespace InfraLogSummaryBucketResolvers {
|
||||
export interface Resolvers {
|
||||
start?: StartResolver /** The start timestamp of the bucket */;
|
||||
end?: EndResolver /** The end timestamp of the bucket */;
|
||||
entriesCount?: EntriesCountResolver /** The number of entries inside the bucket */;
|
||||
}
|
||||
|
||||
export type StartResolver = Resolver<number>;
|
||||
export type EndResolver = Resolver<number>;
|
||||
export type EntriesCountResolver = Resolver<number>;
|
||||
}
|
||||
|
||||
export namespace InfraResponseResolvers {
|
||||
export interface Resolvers {
|
||||
nodes?: NodesResolver;
|
||||
}
|
||||
|
||||
export type NodesResolver = Resolver<InfraNode[], NodesArgs>;
|
||||
export interface NodesArgs {
|
||||
path: InfraPathInput[];
|
||||
metric: InfraMetricInput;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace InfraNodeResolvers {
|
||||
export interface Resolvers {
|
||||
path?: PathResolver;
|
||||
metric?: MetricResolver;
|
||||
}
|
||||
|
||||
export type PathResolver = Resolver<InfraNodePath[]>;
|
||||
export type MetricResolver = Resolver<InfraNodeMetric>;
|
||||
}
|
||||
|
||||
export namespace InfraNodePathResolvers {
|
||||
export interface Resolvers {
|
||||
value?: ValueResolver;
|
||||
}
|
||||
|
||||
export type ValueResolver = Resolver<string>;
|
||||
}
|
||||
|
||||
export namespace InfraNodeMetricResolvers {
|
||||
export interface Resolvers {
|
||||
name?: NameResolver;
|
||||
value?: ValueResolver;
|
||||
}
|
||||
|
||||
export type NameResolver = Resolver<InfraMetricType>;
|
||||
export type ValueResolver = Resolver<number>;
|
||||
}
|
||||
|
||||
export namespace InfraMetricDataResolvers {
|
||||
export interface Resolvers {
|
||||
id?: IdResolver;
|
||||
series?: SeriesResolver;
|
||||
}
|
||||
|
||||
export type IdResolver = Resolver<InfraMetric | null>;
|
||||
export type SeriesResolver = Resolver<InfraDataSeries[]>;
|
||||
}
|
||||
|
||||
export namespace InfraDataSeriesResolvers {
|
||||
export interface Resolvers {
|
||||
id?: IdResolver;
|
||||
data?: DataResolver;
|
||||
}
|
||||
|
||||
export type IdResolver = Resolver<string>;
|
||||
export type DataResolver = Resolver<InfraDataPoint[]>;
|
||||
}
|
||||
|
||||
export namespace InfraDataPointResolvers {
|
||||
export interface Resolvers {
|
||||
timestamp?: TimestampResolver;
|
||||
value?: ValueResolver;
|
||||
}
|
||||
|
||||
export type TimestampResolver = Resolver<number>;
|
||||
export type ValueResolver = Resolver<number | null>;
|
||||
}
|
||||
|
||||
export interface InfraTimeKeyInput {
|
||||
time: number;
|
||||
tiebreaker: number;
|
||||
}
|
||||
|
||||
export interface InfraTimerangeInput {
|
||||
interval: string /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */;
|
||||
to: number /** The end of the timerange */;
|
||||
from: number /** The beginning of the timerange */;
|
||||
}
|
||||
|
||||
export interface InfraPathInput {
|
||||
type: InfraPathType /** The type of path */;
|
||||
label?:
|
||||
| string
|
||||
| null /** The label to use in the results for the group by for the terms group by */;
|
||||
field?:
|
||||
| string
|
||||
| null /** The field to group by from a terms aggregation, this is ignored by the filter type */;
|
||||
filters?: InfraPathFilterInput[] | null /** The fitlers for the filter group by */;
|
||||
}
|
||||
/** A group by filter */
|
||||
export interface InfraPathFilterInput {
|
||||
label: string /** The label for the filter, this will be used as the group name in the final results */;
|
||||
query: string /** The query string query */;
|
||||
}
|
||||
|
||||
export interface InfraMetricInput {
|
||||
type: InfraMetricType /** The type of metric */;
|
||||
}
|
||||
export interface SourceQueryArgs {
|
||||
id: string /** The id of the source */;
|
||||
}
|
||||
export interface CapabilitiesByNodeInfraSourceArgs {
|
||||
nodeName: string;
|
||||
nodeType: InfraNodeType;
|
||||
}
|
||||
export interface LogEntriesAroundInfraSourceArgs {
|
||||
key: InfraTimeKeyInput /** The sort key that corresponds to the point in time */;
|
||||
countBefore?: number | null /** The maximum number of preceding to return */;
|
||||
countAfter?: number | null /** The maximum number of following to return */;
|
||||
filterQuery?: string | null /** The query to filter the log entries by */;
|
||||
highlightQuery?: string | null /** The query to highlight the log entries with */;
|
||||
}
|
||||
export interface LogEntriesBetweenInfraSourceArgs {
|
||||
startKey: InfraTimeKeyInput /** The sort key that corresponds to the start of the interval */;
|
||||
endKey: InfraTimeKeyInput /** The sort key that corresponds to the end of the interval */;
|
||||
filterQuery?: string | null /** The query to filter the log entries by */;
|
||||
highlightQuery?: string | null /** The query to highlight the log entries with */;
|
||||
}
|
||||
export interface LogSummaryBetweenInfraSourceArgs {
|
||||
start: number /** The millisecond timestamp that corresponds to the start of the interval */;
|
||||
end: number /** The millisecond timestamp that corresponds to the end of the interval */;
|
||||
bucketSize: number /** The size of each bucket in milliseconds */;
|
||||
filterQuery?: string | null /** The query to filter the log entries by */;
|
||||
}
|
||||
export interface MapInfraSourceArgs {
|
||||
timerange: InfraTimerangeInput;
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface MetricsInfraSourceArgs {
|
||||
nodeId: string;
|
||||
nodeType: InfraNodeType;
|
||||
timerange: InfraTimerangeInput;
|
||||
metrics: InfraMetric[];
|
||||
}
|
||||
export interface IndexFieldsInfraSourceStatusArgs {
|
||||
indexType?: InfraIndexType | null;
|
||||
}
|
||||
export interface NodesInfraResponseArgs {
|
||||
path: InfraPathInput[];
|
||||
metric: InfraMetricInput;
|
||||
}
|
||||
|
||||
export enum InfraIndexType {
|
||||
ANY = 'ANY',
|
||||
LOGS = 'LOGS',
|
||||
METRICS = 'METRICS',
|
||||
}
|
||||
|
||||
export enum InfraNodeType {
|
||||
pod = 'pod',
|
||||
container = 'container',
|
||||
host = 'host',
|
||||
}
|
||||
|
||||
export enum InfraPathType {
|
||||
terms = 'terms',
|
||||
filters = 'filters',
|
||||
hosts = 'hosts',
|
||||
pods = 'pods',
|
||||
containers = 'containers',
|
||||
}
|
||||
|
||||
export enum InfraMetricType {
|
||||
count = 'count',
|
||||
cpu = 'cpu',
|
||||
load = 'load',
|
||||
memory = 'memory',
|
||||
tx = 'tx',
|
||||
rx = 'rx',
|
||||
logRate = 'logRate',
|
||||
}
|
||||
|
||||
export enum InfraMetric {
|
||||
hostSystemOverview = 'hostSystemOverview',
|
||||
hostCpuUsage = 'hostCpuUsage',
|
||||
hostFilesystem = 'hostFilesystem',
|
||||
hostK8sOverview = 'hostK8sOverview',
|
||||
hostK8sCpuCap = 'hostK8sCpuCap',
|
||||
hostK8sDiskCap = 'hostK8sDiskCap',
|
||||
hostK8sMemoryCap = 'hostK8sMemoryCap',
|
||||
hostK8sPodCap = 'hostK8sPodCap',
|
||||
hostLoad = 'hostLoad',
|
||||
hostMemoryUsage = 'hostMemoryUsage',
|
||||
hostNetworkTraffic = 'hostNetworkTraffic',
|
||||
podOverview = 'podOverview',
|
||||
podCpuUsage = 'podCpuUsage',
|
||||
podMemoryUsage = 'podMemoryUsage',
|
||||
podLogUsage = 'podLogUsage',
|
||||
podNetworkTraffic = 'podNetworkTraffic',
|
||||
containerOverview = 'containerOverview',
|
||||
containerCpuKernel = 'containerCpuKernel',
|
||||
containerCpuUsage = 'containerCpuUsage',
|
||||
containerDiskIOOps = 'containerDiskIOOps',
|
||||
containerDiskIOBytes = 'containerDiskIOBytes',
|
||||
containerMemory = 'containerMemory',
|
||||
containerNetworkTraffic = 'containerNetworkTraffic',
|
||||
nginxHits = 'nginxHits',
|
||||
nginxRequestRate = 'nginxRequestRate',
|
||||
nginxActiveConnections = 'nginxActiveConnections',
|
||||
nginxRequestsPerConnection = 'nginxRequestsPerConnection',
|
||||
}
|
||||
|
||||
export enum InfraOperator {
|
||||
gt = 'gt',
|
||||
gte = 'gte',
|
||||
lt = 'lt',
|
||||
lte = 'lte',
|
||||
eq = 'eq',
|
||||
}
|
||||
/** A segment of the log entry message */
|
||||
export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment;
|
||||
|
||||
export namespace CapabilitiesQuery {
|
||||
export type Variables = {
|
||||
sourceId: string;
|
||||
nodeId: string;
|
||||
nodeType: InfraNodeType;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'InfraSource';
|
||||
id: string;
|
||||
capabilitiesByNode: (CapabilitiesByNode | null)[];
|
||||
};
|
||||
|
||||
export type CapabilitiesByNode = {
|
||||
__typename?: 'InfraNodeCapability';
|
||||
name: string;
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
export namespace MetricsQuery {
|
||||
export type Variables = {
|
||||
sourceId: string;
|
||||
timerange: InfraTimerangeInput;
|
||||
metrics: InfraMetric[];
|
||||
nodeId: string;
|
||||
nodeType: InfraNodeType;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'InfraSource';
|
||||
id: string;
|
||||
metrics: Metrics[];
|
||||
};
|
||||
|
||||
export type Metrics = {
|
||||
__typename?: 'InfraMetricData';
|
||||
id?: InfraMetric | null;
|
||||
series: Series[];
|
||||
};
|
||||
|
||||
export type Series = {
|
||||
__typename?: 'InfraDataSeries';
|
||||
id: string;
|
||||
data: Data[];
|
||||
};
|
||||
|
||||
export type Data = {
|
||||
__typename?: 'InfraDataPoint';
|
||||
timestamp: number;
|
||||
value?: number | null;
|
||||
};
|
||||
}
|
||||
export namespace WaffleNodesQuery {
|
||||
export type Variables = {
|
||||
sourceId: string;
|
||||
timerange: InfraTimerangeInput;
|
||||
filterQuery?: string | null;
|
||||
metric: InfraMetricInput;
|
||||
path: InfraPathInput[];
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'InfraSource';
|
||||
id: string;
|
||||
map?: Map | null;
|
||||
};
|
||||
|
||||
export type Map = {
|
||||
__typename?: 'InfraResponse';
|
||||
nodes: Nodes[];
|
||||
};
|
||||
|
||||
export type Nodes = {
|
||||
__typename?: 'InfraNode';
|
||||
path: Path[];
|
||||
metric: Metric;
|
||||
};
|
||||
|
||||
export type Path = {
|
||||
__typename?: 'InfraNodePath';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type Metric = {
|
||||
__typename?: 'InfraNodeMetric';
|
||||
name: InfraMetricType;
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
export namespace LogEntries {
|
||||
export type Variables = {
|
||||
sourceId?: string | null;
|
||||
timeKey: InfraTimeKeyInput;
|
||||
countBefore?: number | null;
|
||||
countAfter?: number | null;
|
||||
filterQuery?: string | null;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'InfraSource';
|
||||
id: string;
|
||||
logEntriesAround: LogEntriesAround;
|
||||
};
|
||||
|
||||
export type LogEntriesAround = {
|
||||
__typename?: 'InfraLogEntryInterval';
|
||||
start?: Start | null;
|
||||
end?: End | null;
|
||||
hasMoreBefore: boolean;
|
||||
hasMoreAfter: boolean;
|
||||
entries: Entries[];
|
||||
};
|
||||
|
||||
export type Start = InfraTimeKeyFields.Fragment;
|
||||
|
||||
export type End = InfraTimeKeyFields.Fragment;
|
||||
|
||||
export type Entries = {
|
||||
__typename?: 'InfraLogEntry';
|
||||
gid: string;
|
||||
key: Key;
|
||||
message: Message[];
|
||||
};
|
||||
|
||||
export type Key = {
|
||||
__typename?: 'InfraTimeKey';
|
||||
time: number;
|
||||
tiebreaker: number;
|
||||
};
|
||||
|
||||
export type Message =
|
||||
| InfraLogMessageFieldSegmentInlineFragment
|
||||
| InfraLogMessageConstantSegmentInlineFragment;
|
||||
|
||||
export type InfraLogMessageFieldSegmentInlineFragment = {
|
||||
__typename?: 'InfraLogMessageFieldSegment';
|
||||
field: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type InfraLogMessageConstantSegmentInlineFragment = {
|
||||
__typename?: 'InfraLogMessageConstantSegment';
|
||||
constant: string;
|
||||
};
|
||||
}
|
||||
export namespace LogSummary {
|
||||
export type Variables = {
|
||||
sourceId?: string | null;
|
||||
start: number;
|
||||
end: number;
|
||||
bucketSize: number;
|
||||
filterQuery?: string | null;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'InfraSource';
|
||||
id: string;
|
||||
logSummaryBetween: LogSummaryBetween;
|
||||
};
|
||||
|
||||
export type LogSummaryBetween = {
|
||||
__typename?: 'InfraLogSummaryInterval';
|
||||
start?: number | null;
|
||||
end?: number | null;
|
||||
buckets: Buckets[];
|
||||
};
|
||||
|
||||
export type Buckets = {
|
||||
__typename?: 'InfraLogSummaryBucket';
|
||||
start: number;
|
||||
end: number;
|
||||
entriesCount: number;
|
||||
};
|
||||
}
|
||||
export namespace SourceQuery {
|
||||
export type Variables = {
|
||||
sourceId?: string | null;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'InfraSource';
|
||||
configuration: Configuration;
|
||||
status: Status;
|
||||
};
|
||||
|
||||
export type Configuration = {
|
||||
__typename?: 'InfraSourceConfiguration';
|
||||
metricAlias: string;
|
||||
logAlias: string;
|
||||
fields: Fields;
|
||||
};
|
||||
|
||||
export type Fields = {
|
||||
__typename?: 'InfraSourceFields';
|
||||
container: string;
|
||||
host: string;
|
||||
pod: string;
|
||||
};
|
||||
|
||||
export type Status = {
|
||||
__typename?: 'InfraSourceStatus';
|
||||
indexFields: IndexFields[];
|
||||
logIndicesExist: boolean;
|
||||
metricIndicesExist: boolean;
|
||||
};
|
||||
|
||||
export type IndexFields = {
|
||||
__typename?: 'InfraIndexField';
|
||||
name: string;
|
||||
type: string;
|
||||
searchable: boolean;
|
||||
aggregatable: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace InfraTimeKeyFields {
|
||||
export type Fragment = {
|
||||
__typename?: 'InfraTimeKey';
|
||||
time: number;
|
||||
tiebreaker: number;
|
||||
};
|
||||
}
|
8
x-pack/plugins/infra/common/http_api/index.ts
Normal file
8
x-pack/plugins/infra/common/http_api/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './search_results_api';
|
||||
export * from './search_summary_api';
|
37
x-pack/plugins/infra/common/http_api/search_results_api.ts
Normal file
37
x-pack/plugins/infra/common/http_api/search_results_api.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { LogEntryFieldsMapping, LogEntryTime } from '../log_entry';
|
||||
import { SearchResult } from '../log_search_result';
|
||||
import { TimedApiResponse } from './timed_api';
|
||||
|
||||
interface CommonSearchResultsPostPayload {
|
||||
indices: string[];
|
||||
fields: LogEntryFieldsMapping;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface AdjacentSearchResultsApiPostPayload extends CommonSearchResultsPostPayload {
|
||||
target: LogEntryTime;
|
||||
before: number;
|
||||
after: number;
|
||||
}
|
||||
|
||||
export interface AdjacentSearchResultsApiPostResponse extends TimedApiResponse {
|
||||
results: {
|
||||
before: SearchResult[];
|
||||
after: SearchResult[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ContainedSearchResultsApiPostPayload extends CommonSearchResultsPostPayload {
|
||||
start: LogEntryTime;
|
||||
end: LogEntryTime;
|
||||
}
|
||||
|
||||
export interface ContainedSearchResultsApiPostResponse extends TimedApiResponse {
|
||||
results: SearchResult[];
|
||||
}
|
26
x-pack/plugins/infra/common/http_api/search_summary_api.ts
Normal file
26
x-pack/plugins/infra/common/http_api/search_summary_api.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { LogEntryFieldsMapping } from '../log_entry';
|
||||
import { SearchSummaryBucket } from '../log_search_summary';
|
||||
import { SummaryBucketSize } from '../log_summary';
|
||||
import { TimedApiResponse } from './timed_api';
|
||||
|
||||
export interface SearchSummaryApiPostPayload {
|
||||
bucketSize: {
|
||||
unit: SummaryBucketSize;
|
||||
value: number;
|
||||
};
|
||||
fields: LogEntryFieldsMapping;
|
||||
indices: string[];
|
||||
start: number;
|
||||
end: number;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface SearchSummaryApiPostResponse extends TimedApiResponse {
|
||||
buckets: SearchSummaryBucket[];
|
||||
}
|
13
x-pack/plugins/infra/common/http_api/timed_api.ts
Normal file
13
x-pack/plugins/infra/common/http_api/timed_api.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface ApiResponseTimings {
|
||||
[timing: string]: number;
|
||||
}
|
||||
|
||||
export interface TimedApiResponse {
|
||||
timings: ApiResponseTimings;
|
||||
}
|
8
x-pack/plugins/infra/common/log_entry/index.ts
Normal file
8
x-pack/plugins/infra/common/log_entry/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './log_entry';
|
||||
export * from './log_entry_list';
|
63
x-pack/plugins/infra/common/log_entry/log_entry.ts
Normal file
63
x-pack/plugins/infra/common/log_entry/log_entry.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TimeKey } from '../time';
|
||||
|
||||
export interface LogEntry {
|
||||
gid: string;
|
||||
origin: LogEntryOrigin;
|
||||
fields: LogEntryFields;
|
||||
}
|
||||
|
||||
export interface LogEntryOrigin {
|
||||
id: string;
|
||||
index: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface LogEntryFields extends LogEntryTime {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type LogEntryTime = TimeKey;
|
||||
// export interface LogEntryTime {
|
||||
// tiebreaker: number;
|
||||
// time: number;
|
||||
// }
|
||||
|
||||
export interface LogEntryFieldsMapping {
|
||||
message: string;
|
||||
tiebreaker: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export function getLogEntryKey(entry: LogEntry) {
|
||||
return {
|
||||
gid: entry.gid,
|
||||
tiebreaker: entry.fields.tiebreaker,
|
||||
time: entry.fields.time,
|
||||
};
|
||||
}
|
||||
|
||||
export function isEqual(time1: LogEntryTime, time2: LogEntryTime) {
|
||||
return time1.time === time2.time && time1.tiebreaker === time2.tiebreaker;
|
||||
}
|
||||
|
||||
export function isLess(time1: LogEntryTime, time2: LogEntryTime) {
|
||||
return (
|
||||
time1.time < time2.time || (time1.time === time2.time && time1.tiebreaker < time2.tiebreaker)
|
||||
);
|
||||
}
|
||||
|
||||
export function isLessOrEqual(time1: LogEntryTime, time2: LogEntryTime) {
|
||||
return (
|
||||
time1.time < time2.time || (time1.time === time2.time && time1.tiebreaker <= time2.tiebreaker)
|
||||
);
|
||||
}
|
||||
|
||||
export function isBetween(min: LogEntryTime, max: LogEntryTime, operand: LogEntryTime) {
|
||||
return isLessOrEqual(min, operand) && isLessOrEqual(operand, max);
|
||||
}
|
43
x-pack/plugins/infra/common/log_entry/log_entry_list.ts
Normal file
43
x-pack/plugins/infra/common/log_entry/log_entry_list.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getLogEntryKey,
|
||||
isEqual,
|
||||
isLess,
|
||||
isLessOrEqual,
|
||||
LogEntry,
|
||||
LogEntryTime,
|
||||
} from './log_entry';
|
||||
|
||||
export type LogEntryList = LogEntry[];
|
||||
|
||||
export function getIndexNearLogEntry(logEntries: LogEntryList, key: LogEntryTime, highest = false) {
|
||||
let minIndex = 0;
|
||||
let maxIndex = logEntries.length;
|
||||
let currentIndex: number;
|
||||
let currentKey: LogEntryTime;
|
||||
|
||||
while (minIndex < maxIndex) {
|
||||
currentIndex = (minIndex + maxIndex) >>> 1; // tslint:disable-line:no-bitwise
|
||||
currentKey = getLogEntryKey(logEntries[currentIndex]);
|
||||
|
||||
if ((highest ? isLessOrEqual : isLess)(currentKey, key)) {
|
||||
minIndex = currentIndex + 1;
|
||||
} else {
|
||||
maxIndex = currentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return maxIndex;
|
||||
}
|
||||
|
||||
export function getIndexOfLogEntry(logEntries: LogEntry[], key: LogEntryTime) {
|
||||
const index = getIndexNearLogEntry(logEntries, key);
|
||||
const logEntry = logEntries[index];
|
||||
|
||||
return logEntry && isEqual(key, getLogEntryKey(logEntry)) ? index : null;
|
||||
}
|
12
x-pack/plugins/infra/common/log_search_result/index.ts
Normal file
12
x-pack/plugins/infra/common/log_search_result/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export {
|
||||
getSearchResultIndexBeforeTime,
|
||||
getSearchResultIndexAfterTime,
|
||||
getSearchResultKey,
|
||||
SearchResult,
|
||||
} from './log_search_result';
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { bisector } from 'd3-array';
|
||||
|
||||
import { compareToTimeKey, TimeKey } from '../time';
|
||||
|
||||
export interface SearchResult {
|
||||
gid: string;
|
||||
fields: TimeKey;
|
||||
matches: SearchResultFieldMatches;
|
||||
}
|
||||
|
||||
export interface SearchResultFieldMatches {
|
||||
[field: string]: string[];
|
||||
}
|
||||
|
||||
export const getSearchResultKey = (result: SearchResult) =>
|
||||
({
|
||||
gid: result.gid,
|
||||
tiebreaker: result.fields.tiebreaker,
|
||||
time: result.fields.time,
|
||||
} as TimeKey);
|
||||
|
||||
const searchResultTimeBisector = bisector(compareToTimeKey(getSearchResultKey));
|
||||
export const getSearchResultIndexBeforeTime = searchResultTimeBisector.left;
|
||||
export const getSearchResultIndexAfterTime = searchResultTimeBisector.right;
|
7
x-pack/plugins/infra/common/log_search_summary/index.ts
Normal file
7
x-pack/plugins/infra/common/log_search_summary/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { SearchSummaryBucket } from './log_search_summary';
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { SearchResult } from '../log_search_result';
|
||||
|
||||
export interface SearchSummaryBucket {
|
||||
start: number;
|
||||
end: number;
|
||||
count: number;
|
||||
representative: SearchResult;
|
||||
}
|
7
x-pack/plugins/infra/common/log_summary/index.ts
Normal file
7
x-pack/plugins/infra/common/log_summary/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './log_summary';
|
13
x-pack/plugins/infra/common/log_summary/log_summary.ts
Normal file
13
x-pack/plugins/infra/common/log_summary/log_summary.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface LogSummaryBucket {
|
||||
count: number;
|
||||
end: number;
|
||||
start: number;
|
||||
}
|
||||
|
||||
export type SummaryBucketSize = 'y' | 'M' | 'w' | 'd' | 'h' | 'm' | 's';
|
7
x-pack/plugins/infra/common/log_text_scale/index.ts
Normal file
7
x-pack/plugins/infra/common/log_text_scale/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './log_text_scale';
|
15
x-pack/plugins/infra/common/log_text_scale/log_text_scale.ts
Normal file
15
x-pack/plugins/infra/common/log_text_scale/log_text_scale.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type TextScale = 'small' | 'medium' | 'large';
|
||||
|
||||
export function getLabelOfTextScale(textScale: TextScale) {
|
||||
return textScale.charAt(0).toUpperCase() + textScale.slice(1);
|
||||
}
|
||||
|
||||
export function isTextScale(maybeTextScale: string): maybeTextScale is TextScale {
|
||||
return ['small', 'medium', 'large'].includes(maybeTextScale);
|
||||
}
|
10
x-pack/plugins/infra/common/time/index.ts
Normal file
10
x-pack/plugins/infra/common/time/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './time';
|
||||
export * from './time_unit';
|
||||
export * from './time_scale';
|
||||
export * from './time_key';
|
13
x-pack/plugins/infra/common/time/time.ts
Normal file
13
x-pack/plugins/infra/common/time/time.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { timeFormat } from 'd3-time-format';
|
||||
|
||||
const formatDate = timeFormat('%Y-%m-%d %H:%M:%S.%L');
|
||||
|
||||
export function formatTime(time: number) {
|
||||
return formatDate(new Date(time));
|
||||
}
|
79
x-pack/plugins/infra/common/time/time_key.ts
Normal file
79
x-pack/plugins/infra/common/time/time_key.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { ascending, bisector } from 'd3-array';
|
||||
import pick from 'lodash/fp/pick';
|
||||
|
||||
export interface TimeKey {
|
||||
time: number;
|
||||
tiebreaker: number;
|
||||
gid?: string;
|
||||
}
|
||||
|
||||
export type Comparator = (firstValue: any, secondValue: any) => number;
|
||||
|
||||
export const isTimeKey = (value: any): value is TimeKey =>
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.time === 'number' &&
|
||||
typeof value.tiebreaker === 'number';
|
||||
|
||||
export const pickTimeKey = <T extends TimeKey>(value: T): TimeKey =>
|
||||
pick(['time', 'tiebreaker'], value);
|
||||
|
||||
export function compareTimeKeys(
|
||||
firstKey: TimeKey,
|
||||
secondKey: TimeKey,
|
||||
compareValues: Comparator = ascending
|
||||
): number {
|
||||
const timeComparison = compareValues(firstKey.time, secondKey.time);
|
||||
|
||||
if (timeComparison === 0) {
|
||||
const tiebreakerComparison = compareValues(firstKey.tiebreaker, secondKey.tiebreaker);
|
||||
|
||||
if (
|
||||
tiebreakerComparison === 0 &&
|
||||
typeof firstKey.gid !== 'undefined' &&
|
||||
typeof secondKey.gid !== 'undefined'
|
||||
) {
|
||||
return compareValues(firstKey.gid, secondKey.gid);
|
||||
}
|
||||
|
||||
return tiebreakerComparison;
|
||||
}
|
||||
|
||||
return timeComparison;
|
||||
}
|
||||
|
||||
export const compareToTimeKey = <Value>(
|
||||
keyAccessor: (value: Value) => TimeKey,
|
||||
compareValues?: Comparator
|
||||
) => (value: Value, key: TimeKey) => compareTimeKeys(keyAccessor(value), key, compareValues);
|
||||
|
||||
export const getIndexAtTimeKey = <Value>(
|
||||
keyAccessor: (value: Value) => TimeKey,
|
||||
compareValues?: Comparator
|
||||
) => {
|
||||
const comparator = compareToTimeKey(keyAccessor, compareValues);
|
||||
const collectionBisector = bisector(comparator);
|
||||
|
||||
return (collection: Value[], key: TimeKey): number | null => {
|
||||
const index = collectionBisector.left(collection, key);
|
||||
|
||||
if (index >= collection.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (comparator(collection[index], key) !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return index;
|
||||
};
|
||||
};
|
||||
|
||||
export const timeKeyIsBetween = (min: TimeKey, max: TimeKey, operand: TimeKey) =>
|
||||
compareTimeKeys(min, operand) <= 0 && compareTimeKeys(max, operand) >= 0;
|
37
x-pack/plugins/infra/common/time/time_scale.ts
Normal file
37
x-pack/plugins/infra/common/time/time_scale.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { TimeUnit, timeUnitLabels } from './time_unit';
|
||||
|
||||
export interface TimeScale {
|
||||
unit: TimeUnit;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const getMillisOfScale = (scale: TimeScale) => scale.unit * scale.value;
|
||||
|
||||
export const getLabelOfScale = (scale: TimeScale) => `${scale.value}${timeUnitLabels[scale.unit]}`;
|
||||
|
||||
export const decomposeIntoUnits = (time: number, units: TimeUnit[]) =>
|
||||
units.reduce<TimeScale[]>((result, unitMillis) => {
|
||||
const offset = result.reduce(
|
||||
(accumulatedOffset, timeScale) => accumulatedOffset + getMillisOfScale(timeScale),
|
||||
0
|
||||
);
|
||||
const value = Math.floor((time - offset) / unitMillis);
|
||||
|
||||
if (value > 0) {
|
||||
return [
|
||||
...result,
|
||||
{
|
||||
unit: unitMillis,
|
||||
value,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}, []);
|
41
x-pack/plugins/infra/common/time/time_unit.ts
Normal file
41
x-pack/plugins/infra/common/time/time_unit.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export enum TimeUnit {
|
||||
Millisecond = 1,
|
||||
Second = Millisecond * 1000,
|
||||
Minute = Second * 60,
|
||||
Hour = Minute * 60,
|
||||
Day = Hour * 24,
|
||||
Month = Day * 30,
|
||||
Year = Month * 12,
|
||||
}
|
||||
|
||||
export type ElasticsearchTimeUnit = 's' | 'm' | 'h' | 'd' | 'M' | 'y';
|
||||
|
||||
export const timeUnitLabels = {
|
||||
[TimeUnit.Millisecond]: 'ms',
|
||||
[TimeUnit.Second]: 's',
|
||||
[TimeUnit.Minute]: 'm',
|
||||
[TimeUnit.Hour]: 'h',
|
||||
[TimeUnit.Day]: 'd',
|
||||
[TimeUnit.Month]: 'M',
|
||||
[TimeUnit.Year]: 'y',
|
||||
};
|
||||
|
||||
export const elasticSearchTimeUnits: {
|
||||
[key: string]: ElasticsearchTimeUnit;
|
||||
} = {
|
||||
[TimeUnit.Second]: 's',
|
||||
[TimeUnit.Minute]: 'm',
|
||||
[TimeUnit.Hour]: 'h',
|
||||
[TimeUnit.Day]: 'd',
|
||||
[TimeUnit.Month]: 'M',
|
||||
[TimeUnit.Year]: 'y',
|
||||
};
|
||||
|
||||
export const getElasticSearchTimeUnit = (scale: TimeUnit): ElasticsearchTimeUnit =>
|
||||
elasticSearchTimeUnits[scale];
|
13
x-pack/plugins/infra/common/typed_json.ts
Normal file
13
x-pack/plugins/infra/common/typed_json.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type JsonValue = null | boolean | number | string | JsonObject | JsonArray;
|
||||
|
||||
export interface JsonArray extends Array<JsonValue> {}
|
||||
|
||||
export interface JsonObject {
|
||||
[key: string]: JsonValue;
|
||||
}
|
106
x-pack/plugins/infra/docs/arch.md
Normal file
106
x-pack/plugins/infra/docs/arch.md
Normal file
|
@ -0,0 +1,106 @@
|
|||
# Adapter Based Architecture
|
||||
|
||||
## Terms
|
||||
|
||||
In this arch, we use 3 main terms to describe the code:
|
||||
|
||||
- **Libs / Domain Libs** - Business logic & data formatting (though complex formatting might call utils)
|
||||
- **Adapters** - code that directly calls 3rd party APIs and data sources, exposing clean easy to stub APIs
|
||||
- **Composition Files** - composes adapters into libs based on where the code is running
|
||||
- **Implementation layer** - The API such as rest endpoints or graphql schema on the server, and the state management / UI on the client
|
||||
|
||||
## Arch Visual Example
|
||||
|
||||

|
||||
|
||||
## Code Guidelines
|
||||
|
||||
### Libs & Domain Libs:
|
||||
|
||||
This term is used to describe the location of business logic. Each use-case in your app should maintain its own lib.
|
||||
|
||||
Now there are 2 types of libs. A "regular lib" would be something like a lib for interacting with Kibana APIs, with something like a parent app adapter. The other is a "domain lib", and would be something like a hosts, or logging lib that might have composed into it an Elasicsearch adapter.
|
||||
|
||||
For the cases on this application, we might have a Logs, Hosts, Containers, Services, ParentApp, and Settings libs, just as an example. Libs should only have 1 Lib per use-case.
|
||||
|
||||
Libs have, composed into them, adapters, as well as access to other libs. The inter-dependencies on other libs and adapters are explicitly expressed in the types of the lib's constructor arguments to provide static type checking and improve testability. In the following example AdapterInterface would define the required interface of an adapter composed into this lib. Likewise LibInterface would declare the inter-dependency this lib has on other libs:
|
||||
|
||||
```ts
|
||||
new (adapter: AdapterInterface, otherLibs: { lib1: Lib1Interface; lib2: Lib2Interface }): LibInterface
|
||||
```
|
||||
|
||||
Libs must not contain code that depends on APIs and behavior specific to the runtime environment. Any such code should be extracted into an adapter. Any code that does not meet this requirement should be inside an adapter.
|
||||
|
||||
### Adapters
|
||||
|
||||
Adapters are the location of any code to interact with any data sources, or 3rd party API / dependency. An example of code that belongs to an adapter would be anything that interacts with Kibana, or Elasticsearch. This would also include things like, for instance, the browser's local storage.
|
||||
|
||||
**The interface exposed by an adapter should be as domain-specific as possible to reduce the risk of leaking abstraction from the "adapted" technology. Therefore a method like `getHosts()` would be preferable to a `runQuery(filterArgs)` method.** This way the output can be well typed and easily stubbed out in an alternate adapter. This will result in vast improvements in testing reliability and code quality.
|
||||
|
||||
Even adapters though should have required dependencies injected into them for as much as is reasonable. Though this is something that is up to the specific adapter as to what is best on a case-by-case basis.
|
||||
|
||||
An app will in most cases have multiple types of each adapter. As an example, a Lib might have an Elasticsearch-backed adapter as well as an adapter backed by an in-memory store, both of which expose the same interface. This way you can compose a lib to use an in-memory adapter to functional or unit tests in order to have isolated tests that are cleaner / faster / more accurate.
|
||||
|
||||
Adapters can at times be composed into another adapter. This behavior though should be kept to a strict minimum.
|
||||
|
||||
**Acceptable:**
|
||||
|
||||
- An Elasticsearch adapter being passed into Hosts, K8, and logging adapters. The Elasticsearch adapter would then never be exposed directly to a lib.
|
||||
|
||||
**Unacceptable:**
|
||||
|
||||
- A K8 adapter being composed into a hosts adapter, but then k8 also being exposed to a lib.
|
||||
|
||||
The former is acceptable only to abstract shared code between adapters. It is clear that this is acceptable because only other adapters use this code.
|
||||
|
||||
The latter being a "code smell" that indicates there is ether too much logic in your adapter that should be in a lib, or the adapters API is insufficient and should be reconsidered.
|
||||
|
||||
### Composition files
|
||||
|
||||
These files will import all libs and their required adapters to instantiate them in the correct order while passing in the respective dependencies. For a contrived but realistic example, a dev_ui composition file that composes an Elasticsearch adapter into Logs, Hosts, and Services libs, and a dev-ui adapter into ParentApp, and a local-storage adapter into Settings. Then another composition file for Kibana might compose other compatible adapters for use with the Kibana APIs.
|
||||
|
||||
composition files simply export a compose method that returns the composed and initialized libs.
|
||||
|
||||
## File structure
|
||||
|
||||
An example structure might be...
|
||||
|
||||
```
|
||||
|-- infra-ui
|
||||
|-- common
|
||||
| |-- types.ts
|
||||
|
|
||||
|-- server
|
||||
| |-- lib
|
||||
| | |-- adapters
|
||||
| | | |-- hosts
|
||||
| | | | |-- elasticsearch.ts
|
||||
| | | | |-- fake_data.ts
|
||||
| | | |
|
||||
| | | |-- logs
|
||||
| | | | |-- elasticsearch.ts
|
||||
| | | | |-- fake_data.ts
|
||||
| | | |
|
||||
| | | |-- parent_app
|
||||
| | | | |-- kibana_angular // if an adapter has more than one file...
|
||||
| | | | | |-- index.html
|
||||
| | | | | |-- index.ts
|
||||
| | | | |
|
||||
| | | | |-- ui_harness.ts
|
||||
| | | |
|
||||
| | |-- domains
|
||||
| | | |-- hosts.ts
|
||||
| | | |-- logs.ts
|
||||
| | |
|
||||
| | |-- compose
|
||||
| | | |-- dev.ts
|
||||
| | | |-- kibana.ts
|
||||
| | |
|
||||
| | |-- parent_app.ts // a non-domain lib
|
||||
| | |-- lib.ts // a file containing lib type defs
|
||||
|-- public
|
||||
| | ## SAME STRUCTURE AS SERVER
|
||||
```
|
||||
|
||||
Note that in the above adapters have a folder for each adapter type, then inside the implementation of the adapters. The implementation can be a single file, or a directory where index.js is the class that exposes the adapter.
|
||||
`libs/compose/` contains the composition files
|
132
x-pack/plugins/infra/docs/arch_client.md
Normal file
132
x-pack/plugins/infra/docs/arch_client.md
Normal file
|
@ -0,0 +1,132 @@
|
|||
# Client Architecture
|
||||
|
||||
All rules described in the [server-side architecture documentation](docs/arch.md) apply to the client as well. As shown below, the directory structure additionally accommodates the front-end-specific concepts like components and containers.
|
||||
|
||||
## Apps
|
||||
|
||||
The `apps` folder contains the entry point for the UI code, such as for use in Kibana or testing.
|
||||
|
||||
## Components
|
||||
|
||||
- Components should be stateless wherever possible with pages and containers holding state.
|
||||
- Small (less than ~30 lines of JSX and less than ~120 lines total) components should simply get their own file.
|
||||
- If a component gets too large to reason about, and/or needs multiple child components that are only used in this one place, place them all in a folder.
|
||||
- All components, please use Styled-Components. This also applies to small tweaks to EUI, just use `styled(Component)` and the `attrs` method for always used props. For example:
|
||||
|
||||
```jsx
|
||||
export const Toolbar = styled(EuiPanel).attrs({
|
||||
paddingSize: 'none',
|
||||
grow: false,
|
||||
})`
|
||||
margin: -2px;
|
||||
`;
|
||||
```
|
||||
|
||||
However, components that tweak EUI should go into `/public/components/eui/${componentName}`.
|
||||
|
||||
If using an EUI component that has not yet been typed, types should be placed into `/types/eui.d.ts`
|
||||
|
||||
## Containers (Also: [see GraphQL docs](docs/graphql.md))
|
||||
|
||||
- HOC's based on Apollo.
|
||||
- One folder per data type e.g. `host`. Folder name should be singular.
|
||||
- One file per query type.
|
||||
|
||||
## Pages
|
||||
|
||||
- Ideally one file per page, if more files are needed, move into folder containing the page and a layout file.
|
||||
- Pages are class based components.
|
||||
- Should hold most state, and any additional route logic.
|
||||
- Are the only files where components are wrapped by containers. For example:
|
||||
|
||||
```jsx
|
||||
// Simple usage
|
||||
const FancyLogPage = withSearchResults(class FancyLogPage extends React.Component<FancyLogPageProps> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toolbar />
|
||||
<LogView searchResults={/* ... */} />
|
||||
<SearchBar />
|
||||
<>
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
OR, for more complex scenarios:
|
||||
|
||||
```jsx
|
||||
// Advanced usage
|
||||
const ConnectedToolbar = compose(
|
||||
withTimeMutation,
|
||||
withCurrentTime
|
||||
)(Toolbar);
|
||||
|
||||
const ConnectedLogView = compose(
|
||||
withLogEntries,
|
||||
withSearchResults,
|
||||
)(LogView);
|
||||
|
||||
const ConnectedSearchBar = compose(
|
||||
withSearchMutation
|
||||
)(SearchBar);
|
||||
|
||||
interface FancyLogPageProps {}
|
||||
|
||||
class FancyLogPage extends React.Component<FancyLogPageProps> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<ConnectedToolbar />
|
||||
<ConnectedLogView />
|
||||
<ConnectedSearchBar />
|
||||
<>
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Transforms
|
||||
|
||||
- If you need to do some complex data transforms, it is better to put them here than in a utility or lib. Simpler transforms are probably easier to keep in a container.
|
||||
- One file per transform
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
|-- infra-ui
|
||||
|-- common
|
||||
| |-- types.ts
|
||||
|
|
||||
|-- public
|
||||
| |-- components //
|
||||
| | |-- eui // staging area for eui customizations before pushing upstream
|
||||
| | |-- layout // any layout components should be placed in here
|
||||
| | |-- button.tsx
|
||||
| | |-- mega_table // Where mega table is used directly with a data prop, not a composable table
|
||||
| | |-- index.ts
|
||||
| | |-- row.tsx
|
||||
| | |-- table.tsx
|
||||
| | |-- cell.tsx
|
||||
| |
|
||||
| |-- containers
|
||||
| | |-- host
|
||||
| | | |-- index.ts
|
||||
| | | |-- with_all_hosts.ts
|
||||
| | | |-- transforms
|
||||
| | | |-- hosts_to_waffel.ts
|
||||
| | |
|
||||
| | |-- pod
|
||||
| | |-- index.ts
|
||||
| | |-- with_all_pods.ts
|
||||
| |
|
||||
| |-- pages
|
||||
| | |-- home.tsx // the initial page of a plugin is always the `home` page
|
||||
| | |-- hosts.tsx
|
||||
| | |-- logging.tsx
|
||||
| |
|
||||
| |-- utils // utils folder for what utils folders are for ;)
|
||||
| |
|
||||
| |-- lib // covered in [Our code and arch](docs/arch.md)
|
||||
```
|
BIN
x-pack/plugins/infra/docs/assets/arch.png
Normal file
BIN
x-pack/plugins/infra/docs/assets/arch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
53
x-pack/plugins/infra/docs/graphql.md
Normal file
53
x-pack/plugins/infra/docs/graphql.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
# GraphQL In Infra UI
|
||||
|
||||
- The combined graphql schema collected from both the `public` and `server` directories is exported to `common/all.gql_schema.ts` for the purpose of automatic type generation only.
|
||||
|
||||
## Server
|
||||
|
||||
- Under `/server/graphql` there are files for each domain of data's graph schema and resolvers.
|
||||
- Each file has 2 exports `${domain}Schema` e.g. `fieldsSchema`, and `create${domain}Resolvers` e.g. `createFieldResolvers`
|
||||
- `/server/infra_server.ts` imports all schema and resolvers and passing the full schema to the server
|
||||
- Resolvers should be used to call composed libs, rather than directly performing any meaningful amount of data processing.
|
||||
- Resolvers should, however, only pass the required data into libs; that is to say all args for example would not be passed into a lib unless all were needed.
|
||||
|
||||
## Client
|
||||
|
||||
- Under `/public/containers/${domain}/` there is a file for each container. Each file has two exports, the query name e.g. `AllHosts` and the apollo HOC in the pattern of `with${queryName}` e.g. `withAllHosts`. This is done for two reasons:
|
||||
|
||||
1. It makes the code uniform, thus easier to reason about later.
|
||||
2. If reformatting the data using a transform, it lets us re-type the data clearly.
|
||||
|
||||
- Containers should use the apollo props callback to pass ONLY the props and data needed to children. e.g.
|
||||
|
||||
```ts
|
||||
import { Hosts, Pods, HostsAndPods } from '../../common/types';
|
||||
|
||||
// used to generate the `HostsAndPods` type imported above
|
||||
export const hostsAndPods = gql`
|
||||
# ...
|
||||
`;
|
||||
|
||||
type HostsAndPodsProps = {
|
||||
hosts: Hosts;
|
||||
pods: Pods;
|
||||
}
|
||||
|
||||
export const withHostsAndPods = graphql<
|
||||
{},
|
||||
HostsAndPods.Query,
|
||||
HostsAndPods.Variables,
|
||||
HostsAndPodsProps
|
||||
>(hostsAndPods, {
|
||||
props: ({ data, ownProps }) => ({
|
||||
hosts: hostForMap(data && data.hosts ? data.hosts : []),
|
||||
 pods: podsFromHosts(data && data.hosts ? data.hosts : [])
|
||||
...ownProps,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
as `ownProps` are the props passed to the wrapped component, they should just be forwarded.
|
||||
|
||||
## Types
|
||||
|
||||
- The command `yarn build-graphql-types` derives the schema, query and mutation types and stores them in `common/types.ts` for use on both the client and server.
|
56
x-pack/plugins/infra/index.ts
Normal file
56
x-pack/plugins/infra/index.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import JoiNamespace from 'joi';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { getConfigSchema, initServerWithKibana, KbnServer } from './server/kibana.index';
|
||||
|
||||
const APP_ID = 'infra';
|
||||
|
||||
export function infra(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
id: APP_ID,
|
||||
configPrefix: 'xpack.infra',
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
require: ['kibana', 'elasticsearch'],
|
||||
uiExports: {
|
||||
app: {
|
||||
description: 'Explore your infrastructure',
|
||||
icon: 'plugins/infra/images/infra_mono_white.svg',
|
||||
main: 'plugins/infra/app',
|
||||
title: 'InfraOps',
|
||||
listed: false,
|
||||
url: `/app/${APP_ID}#/home`,
|
||||
},
|
||||
home: ['plugins/infra/register_feature'],
|
||||
links: [
|
||||
{
|
||||
description: 'Explore your infrastructure',
|
||||
icon: 'plugins/infra/images/infra_mono_white.svg',
|
||||
id: 'infra:home',
|
||||
order: 8000,
|
||||
title: 'InfraOps',
|
||||
url: `/app/${APP_ID}#/home`,
|
||||
},
|
||||
{
|
||||
description: 'Explore your logs',
|
||||
icon: 'plugins/infra/images/logging_mono_white.svg',
|
||||
id: 'infra:logs',
|
||||
order: 8001,
|
||||
title: 'Logs',
|
||||
url: `/app/${APP_ID}#/logs`,
|
||||
},
|
||||
],
|
||||
},
|
||||
config(Joi: typeof JoiNamespace) {
|
||||
return getConfigSchema(Joi);
|
||||
},
|
||||
init(server: KbnServer) {
|
||||
initServerWithKibana(server);
|
||||
},
|
||||
});
|
||||
}
|
17
x-pack/plugins/infra/package.json
Normal file
17
x-pack/plugins/infra/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"author": "Elastic",
|
||||
"name": "infra",
|
||||
"version": "7.0.0-alpha1",
|
||||
"scripts": {
|
||||
"build-graphql-types": "node scripts/generate_types_from_graphql.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/boom": "3.2.2",
|
||||
"@types/lodash": "^4.14.110"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/color": "^3.0.0",
|
||||
"boom": "3.1.1",
|
||||
"lodash": "^4.17.10"
|
||||
}
|
||||
}
|
7
x-pack/plugins/infra/public/app.ts
Normal file
7
x-pack/plugins/infra/public/app.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 './apps/kibana_app';
|
11
x-pack/plugins/infra/public/apps/kibana_app.ts
Normal file
11
x-pack/plugins/infra/public/apps/kibana_app.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'uiExports/autocompleteProviders';
|
||||
|
||||
import { compose } from '../lib/compose/kibana_compose';
|
||||
import { startApp } from './start_app';
|
||||
startApp(compose());
|
43
x-pack/plugins/infra/public/apps/start_app.tsx
Normal file
43
x-pack/plugins/infra/public/apps/start_app.tsx
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createHashHistory } from 'history';
|
||||
import React from 'react';
|
||||
import { ApolloProvider } from 'react-apollo';
|
||||
import { Provider as ReduxStoreProvider } from 'react-redux';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { pluck } from 'rxjs/operators';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
// TODO use theme provided from parentApp when kibana supports it
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json';
|
||||
import '@elastic/eui/dist/eui_theme_light.css';
|
||||
import { InfraFrontendLibs } from '../lib/lib';
|
||||
import { PageRouter } from '../routes';
|
||||
import { createStore } from '../store';
|
||||
|
||||
export async function startApp(libs: InfraFrontendLibs) {
|
||||
const history = createHashHistory();
|
||||
|
||||
const libs$ = new BehaviorSubject(libs);
|
||||
const store = createStore({
|
||||
apolloClient: libs$.pipe(pluck('apolloClient')),
|
||||
observableApi: libs$.pipe(pluck('observableApi')),
|
||||
});
|
||||
|
||||
libs.framework.render(
|
||||
<EuiErrorBoundary>
|
||||
<ReduxStoreProvider store={store}>
|
||||
<ApolloProvider client={libs.apolloClient}>
|
||||
<ThemeProvider theme={{ eui: euiVars }}>
|
||||
<PageRouter history={history} />
|
||||
</ThemeProvider>
|
||||
</ApolloProvider>
|
||||
</ReduxStoreProvider>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
}
|
9
x-pack/plugins/infra/public/apps/testing_app.ts
Normal file
9
x-pack/plugins/infra/public/apps/testing_app.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { compose } from '../lib/compose/testing_compose';
|
||||
import { startApp } from './start_app';
|
||||
startApp(compose());
|
166
x-pack/plugins/infra/public/components/auto_sizer.tsx
Normal file
166
x-pack/plugins/infra/public/components/auto_sizer.tsx
Normal file
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 isEqual from 'lodash/fp/isEqual';
|
||||
import React from 'react';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
interface Measurement {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
interface Measurements {
|
||||
bounds: Measurement;
|
||||
content: Measurement;
|
||||
}
|
||||
|
||||
interface AutoSizerProps {
|
||||
detectAnyWindowResize?: boolean;
|
||||
bounds?: boolean;
|
||||
content?: boolean;
|
||||
onResize?: (size: Measurements) => void;
|
||||
children: (
|
||||
args: { measureRef: (instance: HTMLElement | null) => any } & Measurements
|
||||
) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface AutoSizerState {
|
||||
boundsMeasurement: Measurement;
|
||||
contentMeasurement: Measurement;
|
||||
}
|
||||
|
||||
export class AutoSizer extends React.PureComponent<AutoSizerProps, AutoSizerState> {
|
||||
public element: HTMLElement | null = null;
|
||||
public resizeObserver: ResizeObserver | null = null;
|
||||
public windowWidth: number = -1;
|
||||
|
||||
public readonly state = {
|
||||
boundsMeasurement: {
|
||||
height: void 0,
|
||||
width: void 0,
|
||||
},
|
||||
contentMeasurement: {
|
||||
height: void 0,
|
||||
width: void 0,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(props: AutoSizerProps) {
|
||||
super(props);
|
||||
if (this.props.detectAnyWindowResize) {
|
||||
window.addEventListener('resize', this.updateMeasurement);
|
||||
}
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.target === this.element) {
|
||||
this.measure(entry);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
if (this.props.detectAnyWindowResize) {
|
||||
window.removeEventListener('resize', this.updateMeasurement);
|
||||
}
|
||||
}
|
||||
|
||||
public measure = (entry: ResizeObserverEntry | null) => {
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { content = true, bounds = false } = this.props;
|
||||
const {
|
||||
boundsMeasurement: previousBoundsMeasurement,
|
||||
contentMeasurement: previousContentMeasurement,
|
||||
} = this.state;
|
||||
|
||||
const boundsRect = bounds ? this.element.getBoundingClientRect() : null;
|
||||
const boundsMeasurement = boundsRect
|
||||
? {
|
||||
height: this.element.getBoundingClientRect().height,
|
||||
width: this.element.getBoundingClientRect().width,
|
||||
}
|
||||
: previousBoundsMeasurement;
|
||||
|
||||
if (
|
||||
this.props.detectAnyWindowResize &&
|
||||
boundsMeasurement &&
|
||||
boundsMeasurement.width &&
|
||||
this.windowWidth !== -1 &&
|
||||
this.windowWidth > window.innerWidth
|
||||
) {
|
||||
const gap = this.windowWidth - window.innerWidth;
|
||||
boundsMeasurement.width = boundsMeasurement.width - gap;
|
||||
}
|
||||
this.windowWidth = window.innerWidth;
|
||||
const contentRect = content && entry ? entry.contentRect : null;
|
||||
const contentMeasurement =
|
||||
contentRect && entry
|
||||
? {
|
||||
height: entry.contentRect.height,
|
||||
width: entry.contentRect.width,
|
||||
}
|
||||
: previousContentMeasurement;
|
||||
|
||||
if (
|
||||
isEqual(boundsMeasurement, previousBoundsMeasurement) &&
|
||||
isEqual(contentMeasurement, previousContentMeasurement)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.resizeObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ boundsMeasurement, contentMeasurement });
|
||||
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize({
|
||||
bounds: boundsMeasurement,
|
||||
content: contentMeasurement,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { children } = this.props;
|
||||
const { boundsMeasurement, contentMeasurement } = this.state;
|
||||
|
||||
return children({
|
||||
bounds: boundsMeasurement,
|
||||
content: contentMeasurement,
|
||||
measureRef: this.storeRef,
|
||||
});
|
||||
}
|
||||
|
||||
private updateMeasurement = () => {
|
||||
window.setTimeout(() => {
|
||||
this.measure(null);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
private storeRef = (element: HTMLElement | null) => {
|
||||
if (this.element && this.resizeObserver) {
|
||||
this.resizeObserver.unobserve(this.element);
|
||||
}
|
||||
|
||||
if (element && this.resizeObserver) {
|
||||
this.resizeObserver.observe(element);
|
||||
}
|
||||
|
||||
this.element = element;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,305 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFieldSearch,
|
||||
EuiFieldSearchProps,
|
||||
EuiOutsideClickDetector,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
|
||||
|
||||
import { composeStateUpdaters } from '../../utils/typed_react';
|
||||
import { SuggestionItem } from './suggestion_item';
|
||||
|
||||
interface AutocompleteFieldProps {
|
||||
isLoadingSuggestions: boolean;
|
||||
isValid: boolean;
|
||||
loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void;
|
||||
onSubmit?: (value: string) => void;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
suggestions: AutocompleteSuggestion[];
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface AutocompleteFieldState {
|
||||
areSuggestionsVisible: boolean;
|
||||
isFocused: boolean;
|
||||
selectedIndex: number | null;
|
||||
}
|
||||
|
||||
export class AutocompleteField extends React.Component<
|
||||
AutocompleteFieldProps,
|
||||
AutocompleteFieldState
|
||||
> {
|
||||
public readonly state: AutocompleteFieldState = {
|
||||
areSuggestionsVisible: false,
|
||||
isFocused: false,
|
||||
selectedIndex: null,
|
||||
};
|
||||
|
||||
private inputElement: HTMLInputElement | null = null;
|
||||
|
||||
public render() {
|
||||
const { suggestions, isLoadingSuggestions, isValid, placeholder, value } = this.props;
|
||||
const { areSuggestionsVisible, selectedIndex } = this.state;
|
||||
|
||||
return (
|
||||
<EuiOutsideClickDetector onOutsideClick={this.handleBlur}>
|
||||
<AutocompleteContainer>
|
||||
<FixedEuiFieldSearch
|
||||
fullWidth
|
||||
inputRef={this.handleChangeInputRef}
|
||||
isLoading={isLoadingSuggestions}
|
||||
isInvalid={!isValid}
|
||||
onChange={this.handleChange}
|
||||
onFocus={this.handleFocus}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onSearch={this.submit}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
/>
|
||||
{areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? (
|
||||
<SuggestionsPanel>
|
||||
{suggestions.map((suggestion, suggestionIndex) => (
|
||||
<SuggestionItem
|
||||
key={suggestion.text}
|
||||
suggestion={suggestion}
|
||||
isSelected={suggestionIndex === selectedIndex}
|
||||
onMouseEnter={this.selectSuggestionAt(suggestionIndex)}
|
||||
onClick={this.applySuggestionAt(suggestionIndex)}
|
||||
/>
|
||||
))}
|
||||
</SuggestionsPanel>
|
||||
) : null}
|
||||
</AutocompleteContainer>
|
||||
</EuiOutsideClickDetector>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) {
|
||||
const hasNewValue = prevProps.value !== this.props.value;
|
||||
const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions;
|
||||
|
||||
if (hasNewValue) {
|
||||
this.updateSuggestions();
|
||||
}
|
||||
|
||||
if (hasNewSuggestions && this.state.isFocused) {
|
||||
this.showSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
private handleChangeInputRef = (element: HTMLInputElement | null) => {
|
||||
this.inputElement = element;
|
||||
};
|
||||
|
||||
private handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.changeValue(evt.currentTarget.value);
|
||||
};
|
||||
|
||||
private handleKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const { suggestions } = this.props;
|
||||
|
||||
switch (evt.key) {
|
||||
case 'ArrowUp':
|
||||
evt.preventDefault();
|
||||
if (suggestions.length > 0) {
|
||||
this.setState(
|
||||
composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
evt.preventDefault();
|
||||
if (suggestions.length > 0) {
|
||||
this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected));
|
||||
} else {
|
||||
this.updateSuggestions();
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
evt.preventDefault();
|
||||
if (this.state.selectedIndex !== null) {
|
||||
this.applySelectedSuggestion();
|
||||
} else {
|
||||
this.submit();
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
evt.preventDefault();
|
||||
this.setState(withSuggestionsHidden);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private handleKeyUp = (evt: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
switch (evt.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowRight':
|
||||
case 'Home':
|
||||
case 'End':
|
||||
this.updateSuggestions();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private handleFocus = () => {
|
||||
this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused));
|
||||
};
|
||||
|
||||
private handleBlur = () => {
|
||||
this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused));
|
||||
};
|
||||
|
||||
private selectSuggestionAt = (index: number) => () => {
|
||||
this.setState(withSuggestionAtIndexSelected(index));
|
||||
};
|
||||
|
||||
private applySelectedSuggestion = () => {
|
||||
if (this.state.selectedIndex !== null) {
|
||||
this.applySuggestionAt(this.state.selectedIndex)();
|
||||
}
|
||||
};
|
||||
|
||||
private applySuggestionAt = (index: number) => () => {
|
||||
const { value, suggestions } = this.props;
|
||||
const selectedSuggestion = suggestions[index];
|
||||
|
||||
if (!selectedSuggestion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue =
|
||||
value.substr(0, selectedSuggestion.start) +
|
||||
selectedSuggestion.text +
|
||||
value.substr(selectedSuggestion.end);
|
||||
|
||||
this.setState(withSuggestionsHidden);
|
||||
this.changeValue(newValue);
|
||||
this.focusInputElement();
|
||||
};
|
||||
|
||||
private changeValue = (value: string) => {
|
||||
const { onChange } = this.props;
|
||||
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
private focusInputElement = () => {
|
||||
if (this.inputElement) {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
private showSuggestions = () => {
|
||||
this.setState(withSuggestionsVisible);
|
||||
};
|
||||
|
||||
private submit = () => {
|
||||
const { isValid, onSubmit, value } = this.props;
|
||||
|
||||
if (isValid && onSubmit) {
|
||||
onSubmit(value);
|
||||
}
|
||||
|
||||
this.setState(withSuggestionsHidden);
|
||||
};
|
||||
|
||||
private updateSuggestions = () => {
|
||||
const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0;
|
||||
this.props.loadSuggestions(this.props.value, inputCursorPosition, 10);
|
||||
};
|
||||
}
|
||||
|
||||
const withPreviousSuggestionSelected = (
|
||||
state: AutocompleteFieldState,
|
||||
props: AutocompleteFieldProps
|
||||
): AutocompleteFieldState => ({
|
||||
...state,
|
||||
selectedIndex:
|
||||
props.suggestions.length === 0
|
||||
? null
|
||||
: state.selectedIndex !== null
|
||||
? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length
|
||||
: Math.max(props.suggestions.length - 1, 0),
|
||||
});
|
||||
|
||||
const withNextSuggestionSelected = (
|
||||
state: AutocompleteFieldState,
|
||||
props: AutocompleteFieldProps
|
||||
): AutocompleteFieldState => ({
|
||||
...state,
|
||||
selectedIndex:
|
||||
props.suggestions.length === 0
|
||||
? null
|
||||
: state.selectedIndex !== null
|
||||
? (state.selectedIndex + 1) % props.suggestions.length
|
||||
: 0,
|
||||
});
|
||||
|
||||
const withSuggestionAtIndexSelected = (suggestionIndex: number) => (
|
||||
state: AutocompleteFieldState,
|
||||
props: AutocompleteFieldProps
|
||||
): AutocompleteFieldState => ({
|
||||
...state,
|
||||
selectedIndex:
|
||||
props.suggestions.length === 0
|
||||
? null
|
||||
: suggestionIndex >= 0 && suggestionIndex < props.suggestions.length
|
||||
? suggestionIndex
|
||||
: 0,
|
||||
});
|
||||
|
||||
const withSuggestionsVisible = (state: AutocompleteFieldState) => ({
|
||||
...state,
|
||||
areSuggestionsVisible: true,
|
||||
});
|
||||
|
||||
const withSuggestionsHidden = (state: AutocompleteFieldState) => ({
|
||||
...state,
|
||||
areSuggestionsVisible: false,
|
||||
selectedIndex: null,
|
||||
});
|
||||
|
||||
const withFocused = (state: AutocompleteFieldState) => ({
|
||||
...state,
|
||||
isFocused: true,
|
||||
});
|
||||
|
||||
const withUnfocused = (state: AutocompleteFieldState) => ({
|
||||
...state,
|
||||
isFocused: false,
|
||||
});
|
||||
|
||||
const FixedEuiFieldSearch: React.SFC<
|
||||
React.InputHTMLAttributes<HTMLInputElement> &
|
||||
EuiFieldSearchProps & {
|
||||
inputRef?: (element: HTMLInputElement | null) => void;
|
||||
onSearch: (value: string) => void;
|
||||
}
|
||||
> = EuiFieldSearch as any;
|
||||
|
||||
const AutocompleteContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const SuggestionsPanel = styled(EuiPanel).attrs({
|
||||
paddingSize: 'none',
|
||||
hasShadow: true,
|
||||
})`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
`;
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './autocomplete_field';
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 { EuiIcon } from '@elastic/eui';
|
||||
import { tint } from 'polished';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
|
||||
|
||||
interface SuggestionItemProps {
|
||||
isSelected?: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
|
||||
suggestion: AutocompleteSuggestion;
|
||||
}
|
||||
|
||||
export class SuggestionItem extends React.Component<SuggestionItemProps> {
|
||||
public static defaultProps: Partial<SuggestionItemProps> = {
|
||||
isSelected: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { isSelected, onClick, onMouseEnter, suggestion } = this.props;
|
||||
|
||||
return (
|
||||
<SuggestionItemContainer
|
||||
isSelected={isSelected}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<SuggestionItemIconField suggestionType={suggestion.type}>
|
||||
<EuiIcon type={getEuiIconType(suggestion.type)} />
|
||||
</SuggestionItemIconField>
|
||||
<SuggestionItemTextField>{suggestion.text}</SuggestionItemTextField>
|
||||
<SuggestionItemDescriptionField
|
||||
dangerouslySetInnerHTML={{ __html: suggestion.description }}
|
||||
/>
|
||||
</SuggestionItemContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SuggestionItemContainer = styled.div<{
|
||||
isSelected?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: ${props => props.theme.eui.euiFontSizeS};
|
||||
height: ${props => props.theme.eui.euiSizeXl};
|
||||
white-space: nowrap;
|
||||
background-color: ${props =>
|
||||
props.isSelected ? props.theme.eui.euiColorLightestShade : 'transparent'};
|
||||
`;
|
||||
|
||||
const SuggestionItemField = styled.div`
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: ${props => props.theme.eui.euiSizeXl};
|
||||
padding: ${props => props.theme.eui.euiSizeXs};
|
||||
`;
|
||||
|
||||
const SuggestionItemIconField = SuggestionItemField.extend<{ suggestionType: string }>`
|
||||
background-color: ${props => tint(0.1, getEuiIconColor(props.theme, props.suggestionType))};
|
||||
color: ${props => getEuiIconColor(props.theme, props.suggestionType)};
|
||||
flex: 0 0 auto;
|
||||
justify-content: center;
|
||||
width: ${props => props.theme.eui.euiSizeXl};
|
||||
`;
|
||||
|
||||
const SuggestionItemTextField = SuggestionItemField.extend`
|
||||
flex: 2 0 0;
|
||||
font-family: ${props => props.theme.eui.euiCodeFontFamily};
|
||||
`;
|
||||
|
||||
const SuggestionItemDescriptionField = SuggestionItemField.extend`
|
||||
flex: 3 0 0;
|
||||
|
||||
p {
|
||||
display: inline;
|
||||
|
||||
span {
|
||||
font-family: ${props => props.theme.eui.euiCodeFontFamily};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const getEuiIconType = (suggestionType: string) => {
|
||||
switch (suggestionType) {
|
||||
case 'field':
|
||||
return 'kqlField';
|
||||
case 'value':
|
||||
return 'kqlValue';
|
||||
case 'recentSearch':
|
||||
return 'search';
|
||||
case 'conjunction':
|
||||
return 'kqlSelector';
|
||||
case 'operator':
|
||||
return 'kqlOperand';
|
||||
default:
|
||||
return 'empty';
|
||||
}
|
||||
};
|
||||
|
||||
const getEuiIconColor = (theme: any, suggestionType: string): string => {
|
||||
switch (suggestionType) {
|
||||
case 'field':
|
||||
return theme.eui.euiColorVis7;
|
||||
case 'value':
|
||||
return theme.eui.euiColorVis0;
|
||||
case 'operator':
|
||||
return theme.eui.euiColorVis1;
|
||||
case 'conjunction':
|
||||
return theme.eui.euiColorVis2;
|
||||
case 'recentSearch':
|
||||
default:
|
||||
return theme.eui.euiColorMediumShade;
|
||||
}
|
||||
};
|
32
x-pack/plugins/infra/public/components/empty_page.tsx
Normal file
32
x-pack/plugins/infra/public/components/empty_page.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
interface EmptyPageProps {
|
||||
message: string;
|
||||
title: string;
|
||||
actionLabel: string;
|
||||
actionUrl: string;
|
||||
}
|
||||
|
||||
export const EmptyPage: React.SFC<EmptyPageProps> = ({
|
||||
actionLabel,
|
||||
actionUrl,
|
||||
message,
|
||||
title,
|
||||
}) => (
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>{title}</h2>}
|
||||
body={<p>{message}</p>}
|
||||
actions={
|
||||
<EuiButton href={actionUrl} color="primary" fill>
|
||||
{actionLabel}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
7
x-pack/plugins/infra/public/components/eui/index.ts
Normal file
7
x-pack/plugins/infra/public/components/eui/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { Toolbar } from './toolbar';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { Toolbar } from './toolbar';
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Toolbar = styled(EuiPanel).attrs({
|
||||
grow: false,
|
||||
paddingSize: 'none',
|
||||
})`
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
border-radius: 0;
|
||||
padding: ${props => props.theme.eui.euiSizeS} ${props => props.theme.eui.euiSizeL};
|
||||
z-index: 1;
|
||||
`;
|
43
x-pack/plugins/infra/public/components/header.tsx
Normal file
43
x-pack/plugins/infra/public/components/header.tsx
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiBreadcrumbDefinition,
|
||||
EuiHeader,
|
||||
EuiHeaderBreadcrumbs,
|
||||
EuiHeaderSection,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface HeaderProps {
|
||||
breadcrumbs?: EuiBreadcrumbDefinition[];
|
||||
}
|
||||
|
||||
export class Header extends React.PureComponent<HeaderProps> {
|
||||
private staticBreadcrumbs = [
|
||||
{
|
||||
href: '#/',
|
||||
text: 'InfraOps',
|
||||
},
|
||||
];
|
||||
|
||||
public render() {
|
||||
const { breadcrumbs = [] } = this.props;
|
||||
|
||||
return (
|
||||
<HeaderWrapper>
|
||||
<EuiHeaderSection>
|
||||
<EuiHeaderBreadcrumbs breadcrumbs={[...this.staticBreadcrumbs, ...breadcrumbs]} />
|
||||
</EuiHeaderSection>
|
||||
</HeaderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const HeaderWrapper = styled(EuiHeader)`
|
||||
height: 29px;
|
||||
`;
|
47
x-pack/plugins/infra/public/components/loading/index.tsx
Normal file
47
x-pack/plugins/infra/public/components/loading/index.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { EuiLoadingChart, EuiPanel, EuiText } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface InfraLoadingProps {
|
||||
text: string;
|
||||
height: number | string;
|
||||
width: number | string;
|
||||
}
|
||||
|
||||
export class InfraLoadingPanel extends React.PureComponent<InfraLoadingProps, {}> {
|
||||
public render() {
|
||||
const { height, text, width } = this.props;
|
||||
return (
|
||||
<InfraLoadingStaticPanel style={{ height, width }}>
|
||||
<InfraLoadingStaticContentPanel>
|
||||
<EuiPanel>
|
||||
<EuiLoadingChart size="m" />
|
||||
<EuiText>
|
||||
<p>{text}</p>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
</InfraLoadingStaticContentPanel>
|
||||
</InfraLoadingStaticPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const InfraLoadingStaticPanel = styled.div`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const InfraLoadingStaticContentPanel = styled.div`
|
||||
flex: 0 0 auto;
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
`;
|
35
x-pack/plugins/infra/public/components/loading_page.tsx
Normal file
35
x-pack/plugins/infra/public/components/loading_page.tsx
Normal file
|
@ -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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { FlexPage } from './page';
|
||||
|
||||
interface LoadingPageProps {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const LoadingPage = ({ message }: LoadingPageProps) => (
|
||||
<FlexPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageContent verticalPosition="center" horizontalPosition="center">
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{message}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</FlexPage>
|
||||
);
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiPopover } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface LogCustomizationMenuState {
|
||||
isShown: boolean;
|
||||
}
|
||||
|
||||
export class LogCustomizationMenu extends React.Component<{}, LogCustomizationMenuState> {
|
||||
public readonly state = {
|
||||
isShown: false,
|
||||
};
|
||||
|
||||
public show = () => {
|
||||
this.setState({
|
||||
isShown: true,
|
||||
});
|
||||
};
|
||||
|
||||
public hide = () => {
|
||||
this.setState({
|
||||
isShown: false,
|
||||
});
|
||||
};
|
||||
|
||||
public toggleVisibility = () => {
|
||||
this.setState(state => ({
|
||||
isShown: !state.isShown,
|
||||
}));
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { children } = this.props;
|
||||
const { isShown } = this.state;
|
||||
|
||||
const menuButton = (
|
||||
<EuiButtonEmpty color="text" iconType="gear" onClick={this.toggleVisibility} size="xs">
|
||||
Customize
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="customizePopover"
|
||||
button={menuButton}
|
||||
closePopover={this.hide}
|
||||
isOpen={isShown}
|
||||
anchorPosition="downRight"
|
||||
ownFocus
|
||||
>
|
||||
<CustomizationMenuContent>{children}</CustomizationMenuContent>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CustomizationMenuContent = styled.div`
|
||||
min-width: 200px;
|
||||
`;
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { scaleLinear, scaleTime } from 'd3-scale';
|
||||
import { area, curveMonotoneY } from 'd3-shape';
|
||||
import max from 'lodash/fp/max';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { SummaryBucket } from './types';
|
||||
|
||||
interface DensityChartProps {
|
||||
buckets: SummaryBucket[];
|
||||
end: number;
|
||||
start: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const DensityChart: React.SFC<DensityChartProps> = ({
|
||||
buckets,
|
||||
start,
|
||||
end,
|
||||
width,
|
||||
height,
|
||||
}) => {
|
||||
if (start >= end || height <= 0 || width <= 0 || buckets.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const yScale = scaleTime()
|
||||
.domain([start, end])
|
||||
.range([0, height]);
|
||||
|
||||
const xMax = max(buckets.map(bucket => bucket.entriesCount)) || 0;
|
||||
const xScale = scaleLinear()
|
||||
.domain([0, xMax])
|
||||
.range([0, width / 2]);
|
||||
|
||||
const path = area<SummaryBucket>()
|
||||
.x0(xScale(0))
|
||||
.x1(bucket => xScale(bucket.entriesCount))
|
||||
.y(bucket => yScale((bucket.start + bucket.end) / 2))
|
||||
.curve(curveMonotoneY);
|
||||
const pathData = path(buckets);
|
||||
|
||||
return (
|
||||
<g transform={`translate(${width / 2}, 0)`}>
|
||||
<PositiveAreaPath d={pathData || ''} />
|
||||
<NegativeAreaPath transform="scale(-1, 1)" d={pathData || ''} />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const PositiveAreaPath = styled.path`
|
||||
fill: ${props => props.theme.eui.euiColorLightShade};
|
||||
`;
|
||||
|
||||
const NegativeAreaPath = styled.path`
|
||||
fill: ${props => props.theme.eui.euiColorLightestShade};
|
||||
`;
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface HighlightedIntervalProps {
|
||||
className?: string;
|
||||
getPositionOfTime: (time: number) => number;
|
||||
start: number;
|
||||
end: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const HighlightedInterval: React.SFC<HighlightedIntervalProps> = ({
|
||||
className,
|
||||
end,
|
||||
getPositionOfTime,
|
||||
start,
|
||||
width,
|
||||
}) => {
|
||||
const yStart = getPositionOfTime(start);
|
||||
const yEnd = getPositionOfTime(end);
|
||||
|
||||
return (
|
||||
<HighlightPolygon
|
||||
className={className}
|
||||
points={`0,${yStart} ${width},${yStart} ${width},${yEnd} 0,${yEnd}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
HighlightedInterval.displayName = 'HighlightedInterval';
|
||||
|
||||
const HighlightPolygon = styled.polygon`
|
||||
fill: ${props => props.theme.eui.euiColorPrimary};
|
||||
fill-opacity: 0.3;
|
||||
stroke: ${props => props.theme.eui.euiColorPrimary};
|
||||
stroke-width: 1;
|
||||
`;
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { LogMinimap } from './log_minimap';
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 { scaleLinear } from 'd3-scale';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { LogEntryTime } from '../../../../common/log_entry';
|
||||
// import { SearchSummaryBucket } from '../../../../common/log_search_summary';
|
||||
import { DensityChart } from './density_chart';
|
||||
import { HighlightedInterval } from './highlighted_interval';
|
||||
// import { SearchMarkers } from './search_markers';
|
||||
import { TimeRuler } from './time_ruler';
|
||||
import { SummaryBucket } from './types';
|
||||
|
||||
interface LogMinimapProps {
|
||||
className?: string;
|
||||
height: number;
|
||||
highlightedInterval: {
|
||||
end: number;
|
||||
start: number;
|
||||
} | null;
|
||||
jumpToTarget: (params: LogEntryTime) => any;
|
||||
reportVisibleInterval: (
|
||||
params: {
|
||||
start: number;
|
||||
end: number;
|
||||
bucketsOnPage: number;
|
||||
pagesBeforeStart: number;
|
||||
pagesAfterEnd: number;
|
||||
}
|
||||
) => any;
|
||||
intervalSize: number;
|
||||
summaryBuckets: SummaryBucket[];
|
||||
// searchSummaryBuckets?: SearchSummaryBucket[];
|
||||
target: number | null;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export class LogMinimap extends React.Component<LogMinimapProps> {
|
||||
public handleClick: React.MouseEventHandler<SVGSVGElement> = event => {
|
||||
const svgPosition = event.currentTarget.getBoundingClientRect();
|
||||
const clickedYPosition = event.clientY - svgPosition.top;
|
||||
const clickedTime = Math.floor(this.getYScale().invert(clickedYPosition));
|
||||
|
||||
this.props.jumpToTarget({
|
||||
tiebreaker: 0,
|
||||
time: clickedTime,
|
||||
});
|
||||
};
|
||||
|
||||
public getYScale = () => {
|
||||
const { height, intervalSize, target } = this.props;
|
||||
|
||||
const domainStart = target ? target - intervalSize / 2 : 0;
|
||||
const domainEnd = target ? target + intervalSize / 2 : 0;
|
||||
return scaleLinear()
|
||||
.domain([domainStart, domainEnd])
|
||||
.range([0, height]);
|
||||
};
|
||||
|
||||
public getPositionOfTime = (time: number) => {
|
||||
const { height, intervalSize } = this.props;
|
||||
|
||||
const [minTime] = this.getYScale().domain();
|
||||
|
||||
return ((time - minTime) * height) / intervalSize;
|
||||
};
|
||||
|
||||
public updateVisibleInterval = () => {
|
||||
const { summaryBuckets, intervalSize } = this.props;
|
||||
const [minTime, maxTime] = this.getYScale().domain();
|
||||
|
||||
const firstBucket = summaryBuckets[0];
|
||||
const lastBucket = summaryBuckets[summaryBuckets.length - 1];
|
||||
|
||||
const pagesBeforeStart = firstBucket ? (minTime - firstBucket.start) / intervalSize : 0;
|
||||
const pagesAfterEnd = lastBucket ? (lastBucket.end - maxTime) / intervalSize : 0;
|
||||
const bucketsOnPage = firstBucket
|
||||
? (maxTime - minTime) / (firstBucket.end - firstBucket.start)
|
||||
: 0;
|
||||
|
||||
this.props.reportVisibleInterval({
|
||||
end: Math.ceil(maxTime),
|
||||
start: Math.floor(minTime),
|
||||
bucketsOnPage,
|
||||
pagesBeforeStart,
|
||||
pagesAfterEnd,
|
||||
});
|
||||
};
|
||||
|
||||
public componentDidUpdate(prevProps: LogMinimapProps) {
|
||||
const hasNewTarget = prevProps.target !== this.props.target;
|
||||
const hasNewIntervalSize = prevProps.intervalSize !== this.props.intervalSize;
|
||||
|
||||
if (hasNewTarget || hasNewIntervalSize) {
|
||||
this.updateVisibleInterval();
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
className,
|
||||
height,
|
||||
highlightedInterval,
|
||||
// jumpToTarget,
|
||||
summaryBuckets,
|
||||
// searchSummaryBuckets,
|
||||
width,
|
||||
} = this.props;
|
||||
|
||||
const [minTime, maxTime] = this.getYScale().domain();
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
height={height}
|
||||
preserveAspectRatio="none"
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
width={width}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<MinimapBackground x={width / 2} y="0" width={width / 2} height={height} />
|
||||
<DensityChart
|
||||
buckets={summaryBuckets}
|
||||
start={minTime}
|
||||
end={maxTime}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
<MinimapBorder x1={width / 2} y1={0} x2={width / 2} y2={height} />
|
||||
<TimeRuler start={minTime} end={maxTime} width={width} height={height} tickCount={12} />
|
||||
{highlightedInterval ? (
|
||||
<HighlightedInterval
|
||||
end={highlightedInterval.end}
|
||||
getPositionOfTime={this.getPositionOfTime}
|
||||
start={highlightedInterval.start}
|
||||
width={width}
|
||||
/>
|
||||
) : null}
|
||||
{/*<g transform={`translate(${width * 0.5}, 0)`}>
|
||||
<SearchMarkers
|
||||
buckets={searchSummaryBuckets || []}
|
||||
start={minTime}
|
||||
end={maxTime}
|
||||
width={width / 2}
|
||||
height={height}
|
||||
jumpToTarget={jumpToTarget}
|
||||
/>
|
||||
</g>*/}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MinimapBackground = styled.rect`
|
||||
fill: ${props => props.theme.eui.euiColorLightestShade};
|
||||
`;
|
||||
|
||||
const MinimapBorder = styled.line`
|
||||
stroke: ${props => props.theme.eui.euiColorMediumShade};
|
||||
stroke-width: 1px;
|
||||
`;
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
import { LogEntryTime } from '../../../../common/log_entry';
|
||||
import { SearchSummaryBucket } from '../../../../common/log_search_summary';
|
||||
import { SearchMarkerTooltip } from './search_marker_tooltip';
|
||||
|
||||
interface SearchMarkerProps {
|
||||
bucket: SearchSummaryBucket;
|
||||
height: number;
|
||||
width: number;
|
||||
jumpToTarget: (target: LogEntryTime) => void;
|
||||
}
|
||||
|
||||
interface SearchMarkerState {
|
||||
hoveredPosition: ClientRect | null;
|
||||
}
|
||||
|
||||
export class SearchMarker extends React.PureComponent<SearchMarkerProps, SearchMarkerState> {
|
||||
public readonly state = {
|
||||
hoveredPosition: null,
|
||||
};
|
||||
|
||||
public handleClick: React.MouseEventHandler<SVGGElement> = evt => {
|
||||
evt.stopPropagation();
|
||||
|
||||
this.props.jumpToTarget(this.props.bucket.representative.fields);
|
||||
};
|
||||
|
||||
public handleMouseEnter: React.MouseEventHandler<SVGGElement> = evt => {
|
||||
this.setState({
|
||||
hoveredPosition: evt.currentTarget.getBoundingClientRect(),
|
||||
});
|
||||
};
|
||||
|
||||
public handleMouseLeave: React.MouseEventHandler<SVGGElement> = () => {
|
||||
this.setState({
|
||||
hoveredPosition: null,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { bucket, height, width } = this.props;
|
||||
const { hoveredPosition } = this.state;
|
||||
|
||||
const bulge =
|
||||
bucket.count > 1 ? (
|
||||
<SearchMarkerForegroundRect x="-2" y="-2" width="4" height={height + 2} rx="2" ry="2" />
|
||||
) : (
|
||||
<>
|
||||
<SearchMarkerForegroundRect x="-1" y="0" width="2" height={height} />
|
||||
<SearchMarkerForegroundRect
|
||||
x="-2"
|
||||
y={height / 2 - 2}
|
||||
width="4"
|
||||
height="4"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hoveredPosition ? (
|
||||
<SearchMarkerTooltip markerPosition={hoveredPosition}>
|
||||
{bucket.count} {bucket.count === 1 ? 'search result' : 'search results'}
|
||||
</SearchMarkerTooltip>
|
||||
) : null}
|
||||
<SearchMarkerGroup
|
||||
onClick={this.handleClick}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<SearchMarkerBackgroundRect x="0" y="0" width={width} height={height} />
|
||||
{bulge}
|
||||
</SearchMarkerGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fadeInAnimation = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchMarkerGroup = styled.g`
|
||||
animation: ${fadeInAnimation} ${props => props.theme.eui.euiAnimSpeedExtraSlow} ease-in both;
|
||||
`;
|
||||
|
||||
const SearchMarkerBackgroundRect = styled.rect`
|
||||
fill: ${props => props.theme.eui.euiColorSecondary};
|
||||
opacity: 0;
|
||||
transition: opacity ${props => props.theme.eui.euiAnimSpeedNormal} ease-in;
|
||||
|
||||
${SearchMarkerGroup}:hover & {
|
||||
opacity: 0.2;
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchMarkerForegroundRect = styled.rect`
|
||||
fill: ${props => props.theme.eui.euiColorSecondary};
|
||||
`;
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { calculatePopoverPosition, EuiPortal } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AutoSizer } from '../../auto_sizer';
|
||||
|
||||
interface SearchMarkerTooltipProps {
|
||||
markerPosition: ClientRect;
|
||||
}
|
||||
|
||||
export class SearchMarkerTooltip extends React.PureComponent<SearchMarkerTooltipProps, {}> {
|
||||
public render() {
|
||||
const { children, markerPosition } = this.props;
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<AutoSizer content={false} bounds>
|
||||
{({ measureRef, bounds: { width, height } }) => {
|
||||
const { top, left } =
|
||||
width && height
|
||||
? calculatePopoverPosition(markerPosition, { width, height }, 'left', 16, [
|
||||
'left',
|
||||
])
|
||||
: {
|
||||
left: -9999, // render off-screen before the first measurement
|
||||
top: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="euiToolTip euiToolTip--left euiToolTipPopover"
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
}}
|
||||
ref={measureRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</EuiPortal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import { scaleTime } from 'd3-scale';
|
||||
import * as React from 'react';
|
||||
|
||||
import { LogEntryTime } from '../../../../common/log_entry';
|
||||
import { SearchSummaryBucket } from '../../../../common/log_search_summary';
|
||||
import { SearchMarker } from './search_marker';
|
||||
|
||||
interface SearchMarkersProps {
|
||||
buckets: SearchSummaryBucket[];
|
||||
className?: string;
|
||||
end: number;
|
||||
start: number;
|
||||
width: number;
|
||||
height: number;
|
||||
jumpToTarget: (target: LogEntryTime) => void;
|
||||
}
|
||||
|
||||
export class SearchMarkers extends React.PureComponent<SearchMarkersProps, {}> {
|
||||
public render() {
|
||||
const { buckets, start, end, width, height, jumpToTarget, className } = this.props;
|
||||
const classes = classNames('minimapSearchMarkers', className);
|
||||
|
||||
if (start >= end || height <= 0 || Object.keys(buckets).length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const yScale = scaleTime()
|
||||
.domain([start, end])
|
||||
.range([0, height]);
|
||||
|
||||
return (
|
||||
<g className={classes}>
|
||||
{buckets.map(bucket => (
|
||||
<g key={bucket.representative.gid} transform={`translate(0, ${yScale(bucket.start)})`}>
|
||||
<SearchMarker
|
||||
bucket={bucket}
|
||||
height={yScale(bucket.end) - yScale(bucket.start)}
|
||||
width={width}
|
||||
jumpToTarget={jumpToTarget}
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { scaleTime } from 'd3-scale';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface TimeRulerProps {
|
||||
end: number;
|
||||
height: number;
|
||||
start: number;
|
||||
tickCount: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const TimeRuler: React.SFC<TimeRulerProps> = ({ end, height, start, tickCount, width }) => {
|
||||
const yScale = scaleTime()
|
||||
.domain([start, end])
|
||||
.range([0, height]);
|
||||
|
||||
const ticks = yScale.ticks(tickCount);
|
||||
const formatTick = yScale.tickFormat();
|
||||
|
||||
return (
|
||||
<g>
|
||||
{ticks.map((tick, tickIndex) => {
|
||||
const y = yScale(tick);
|
||||
return (
|
||||
<g key={`tick${tickIndex}`}>
|
||||
<TimeRulerTickLabel x={2} y={y - 4}>
|
||||
{formatTick(tick)}
|
||||
</TimeRulerTickLabel>
|
||||
<TimeRulerGridLine x1={0} y1={y} x2={width} y2={y} />
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
TimeRuler.displayName = 'TimeRuler';
|
||||
|
||||
const TimeRulerTickLabel = styled.text`
|
||||
font-size: ${props => props.theme.eui.euiFontSizeXs};
|
||||
line-height: ${props => props.theme.eui.euiLineHeight};
|
||||
color: ${props => props.theme.eui.euiTextColor};
|
||||
`;
|
||||
|
||||
const TimeRulerGridLine = styled.line`
|
||||
stroke: ${props => props.theme.eui.euiColorMediumShade};
|
||||
stroke-dasharray: 2, 2;
|
||||
stroke-opacity: 0.5;
|
||||
stroke-width: 1px;
|
||||
`;
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface SummaryBucket {
|
||||
start: number;
|
||||
end: number;
|
||||
entriesCount: number;
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiRadioGroup } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
|
||||
interface IntervalSizeDescriptor {
|
||||
label: string;
|
||||
intervalSize: number;
|
||||
}
|
||||
|
||||
interface LogMinimapScaleControlsProps {
|
||||
availableIntervalSizes: IntervalSizeDescriptor[];
|
||||
intervalSize: number;
|
||||
setIntervalSize: (intervalSize: number) => any;
|
||||
}
|
||||
|
||||
export class LogMinimapScaleControls extends React.PureComponent<LogMinimapScaleControlsProps> {
|
||||
public handleScaleChange = (intervalSizeDescriptorKey: string) => {
|
||||
const { availableIntervalSizes, setIntervalSize } = this.props;
|
||||
const [sizeDescriptor] = availableIntervalSizes.filter(
|
||||
intervalKeyEquals(intervalSizeDescriptorKey)
|
||||
);
|
||||
|
||||
if (sizeDescriptor) {
|
||||
setIntervalSize(sizeDescriptor.intervalSize);
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { availableIntervalSizes, intervalSize } = this.props;
|
||||
const [currentSizeDescriptor] = availableIntervalSizes.filter(intervalSizeEquals(intervalSize));
|
||||
|
||||
return (
|
||||
<EuiFormRow label="Minimap Scale">
|
||||
<EuiRadioGroup
|
||||
options={availableIntervalSizes.map(sizeDescriptor => ({
|
||||
id: getIntervalSizeDescriptorKey(sizeDescriptor),
|
||||
label: sizeDescriptor.label,
|
||||
}))}
|
||||
onChange={this.handleScaleChange}
|
||||
idSelected={getIntervalSizeDescriptorKey(currentSizeDescriptor)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getIntervalSizeDescriptorKey = (sizeDescriptor: IntervalSizeDescriptor) =>
|
||||
`${sizeDescriptor.intervalSize}`;
|
||||
|
||||
const intervalKeyEquals = (key: string) => (sizeDescriptor: IntervalSizeDescriptor) =>
|
||||
getIntervalSizeDescriptorKey(sizeDescriptor) === key;
|
||||
|
||||
const intervalSizeEquals = (size: number) => (sizeDescriptor: IntervalSizeDescriptor) =>
|
||||
sizeDescriptor.intervalSize === size;
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { LogSearchControls } from './log_search_controls';
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
import { LogEntryTime } from '../../../../common/log_entry';
|
||||
|
||||
interface LogSearchButtonsProps {
|
||||
className?: string;
|
||||
jumpToTarget: (target: LogEntryTime) => void;
|
||||
previousSearchResult: LogEntryTime | null;
|
||||
nextSearchResult: LogEntryTime | null;
|
||||
}
|
||||
|
||||
export class LogSearchButtons extends React.PureComponent<LogSearchButtonsProps, {}> {
|
||||
public handleJumpToPreviousSearchResult: React.MouseEventHandler<HTMLButtonElement> = () => {
|
||||
const { jumpToTarget, previousSearchResult } = this.props;
|
||||
|
||||
if (previousSearchResult) {
|
||||
jumpToTarget(previousSearchResult);
|
||||
}
|
||||
};
|
||||
|
||||
public handleJumpToNextSearchResult: React.MouseEventHandler<HTMLButtonElement> = () => {
|
||||
const { jumpToTarget, nextSearchResult } = this.props;
|
||||
|
||||
if (nextSearchResult) {
|
||||
jumpToTarget(nextSearchResult);
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { className, previousSearchResult, nextSearchResult } = this.props;
|
||||
|
||||
const classes = classNames('searchButtons', className);
|
||||
const hasPreviousSearchResult = !!previousSearchResult;
|
||||
const hasNextSearchResult = !!nextSearchResult;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup className={classes} gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
onClick={this.handleJumpToPreviousSearchResult}
|
||||
iconType="arrowLeft"
|
||||
iconSide="left"
|
||||
isDisabled={!hasPreviousSearchResult}
|
||||
size="s"
|
||||
>
|
||||
Previous
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
onClick={this.handleJumpToNextSearchResult}
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
isDisabled={!hasNextSearchResult}
|
||||
size="s"
|
||||
>
|
||||
Next
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
import { LogEntryTime } from '../../../../common/log_entry';
|
||||
import { LogSearchButtons } from './log_search_buttons';
|
||||
import { LogSearchInput } from './log_search_input';
|
||||
|
||||
interface LogSearchControlsProps {
|
||||
className?: string;
|
||||
clearSearch: () => any;
|
||||
isLoadingSearchResults: boolean;
|
||||
previousSearchResult: LogEntryTime | null;
|
||||
nextSearchResult: LogEntryTime | null;
|
||||
jumpToTarget: (target: LogEntryTime) => any;
|
||||
search: (query: string) => any;
|
||||
}
|
||||
|
||||
export class LogSearchControls extends React.PureComponent<LogSearchControlsProps, {}> {
|
||||
public render() {
|
||||
const {
|
||||
className,
|
||||
clearSearch,
|
||||
isLoadingSearchResults,
|
||||
previousSearchResult,
|
||||
nextSearchResult,
|
||||
jumpToTarget,
|
||||
search,
|
||||
} = this.props;
|
||||
|
||||
const classes = classNames('searchControls', className);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
justifyContent="flexStart"
|
||||
className={classes}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<LogSearchInput
|
||||
isLoading={isLoadingSearchResults}
|
||||
onClear={clearSearch}
|
||||
onSearch={search}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LogSearchButtons
|
||||
previousSearchResult={previousSearchResult}
|
||||
nextSearchResult={nextSearchResult}
|
||||
jumpToTarget={jumpToTarget}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { EuiFieldSearch } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface LogSearchInputProps {
|
||||
className?: string;
|
||||
isLoading: boolean;
|
||||
onSearch: (query: string) => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
interface LogSearchInputState {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export class LogSearchInput extends React.PureComponent<LogSearchInputProps, LogSearchInputState> {
|
||||
public readonly state = {
|
||||
query: '',
|
||||
};
|
||||
|
||||
public handleSubmit: React.FormEventHandler<HTMLFormElement> = evt => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { query } = this.state;
|
||||
|
||||
if (query === '') {
|
||||
this.props.onClear();
|
||||
} else {
|
||||
this.props.onSearch(this.state.query);
|
||||
}
|
||||
};
|
||||
|
||||
public handleChangeQuery: React.ChangeEventHandler<HTMLInputElement> = evt => {
|
||||
this.setState({
|
||||
query: evt.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { className, isLoading } = this.props;
|
||||
const { query } = this.state;
|
||||
|
||||
const classes = classNames('loggingSearchInput', className);
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<PlainSearchField
|
||||
aria-label="search"
|
||||
className={classes}
|
||||
fullWidth
|
||||
isLoading={isLoading}
|
||||
onChange={this.handleChangeQuery}
|
||||
placeholder="Search"
|
||||
value={query}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const PlainSearchField = styled(EuiFieldSearch)`
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 -2px 0 0 ${props => props.theme.eui.euiColorPrimary};
|
||||
}
|
||||
`;
|
|
@ -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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const LogStatusbar = styled(EuiFlexGroup).attrs({
|
||||
alignItems: 'center',
|
||||
gutterSize: 'none',
|
||||
justifyContent: 'flexEnd',
|
||||
})`
|
||||
padding: ${props => props.theme.eui.euiSizeS};
|
||||
border-top: ${props => props.theme.eui.euiBorderThin};
|
||||
max-height: 48px;
|
||||
min-height: 48px;
|
||||
background-color: ${props => props.theme.eui.euiColorEmptyShade};
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
export const LogStatusbarItem = EuiFlexItem;
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiRadioGroup } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
|
||||
import { getLabelOfTextScale, isTextScale, TextScale } from '../../../common/log_text_scale';
|
||||
|
||||
interface LogTextScaleControlsProps {
|
||||
availableTextScales: TextScale[];
|
||||
textScale: TextScale;
|
||||
setTextScale: (scale: TextScale) => any;
|
||||
}
|
||||
|
||||
export class LogTextScaleControls extends React.PureComponent<LogTextScaleControlsProps> {
|
||||
public setTextScale = (textScale: string) => {
|
||||
if (isTextScale(textScale)) {
|
||||
this.props.setTextScale(textScale);
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { availableTextScales, textScale } = this.props;
|
||||
|
||||
return (
|
||||
<EuiFormRow label="Text Size">
|
||||
<EuiRadioGroup
|
||||
options={availableTextScales.map((availableTextScale: TextScale) => ({
|
||||
id: availableTextScale.toString(),
|
||||
label: getLabelOfTextScale(availableTextScale),
|
||||
}))}
|
||||
idSelected={textScale}
|
||||
onChange={this.setTextScale}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
|
||||
interface LogTextStreamEmptyViewProps {
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
export class LogTextStreamEmptyView extends React.PureComponent<LogTextStreamEmptyViewProps> {
|
||||
public render() {
|
||||
const { reload } = this.props;
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>There are no log messages to display.</h2>}
|
||||
titleSize="m"
|
||||
body={<p>Try adjusting your filter.</p>}
|
||||
actions={
|
||||
<EuiButton iconType="refresh" color="primary" fill onClick={reload}>
|
||||
Check for new data
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ScrollableLogTextStreamView } from './scrollable_log_text_stream_view';
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { bisector } from 'd3-array';
|
||||
|
||||
import { getLogEntryKey, LogEntry } from '../../../../common/log_entry';
|
||||
import { SearchResult } from '../../../../common/log_search_result';
|
||||
import { compareToTimeKey, TimeKey } from '../../../../common/time';
|
||||
|
||||
export type StreamItem = LogEntryStreamItem;
|
||||
|
||||
export interface LogEntryStreamItem {
|
||||
kind: 'logEntry';
|
||||
logEntry: LogEntry;
|
||||
searchResult: SearchResult | undefined;
|
||||
}
|
||||
|
||||
export function getStreamItemTimeKey(item: StreamItem) {
|
||||
switch (item.kind) {
|
||||
case 'logEntry':
|
||||
return getLogEntryKey(item.logEntry);
|
||||
}
|
||||
}
|
||||
|
||||
export function getStreamItemId(item: StreamItem) {
|
||||
const { time, tiebreaker, gid } = getStreamItemTimeKey(item);
|
||||
|
||||
return `${time}:${tiebreaker}:${gid}`;
|
||||
}
|
||||
|
||||
export function parseStreamItemId(id: string) {
|
||||
const idFragments = id.split(':');
|
||||
|
||||
return {
|
||||
gid: idFragments.slice(2).join(':'),
|
||||
tiebreaker: parseInt(idFragments[1], 10),
|
||||
time: parseInt(idFragments[0], 10),
|
||||
};
|
||||
}
|
||||
|
||||
const streamItemTimeBisector = bisector(compareToTimeKey(getStreamItemTimeKey));
|
||||
|
||||
export const getStreamItemBeforeTimeKey = (streamItems: StreamItem[], key: TimeKey) =>
|
||||
streamItems[Math.min(streamItemTimeBisector.left(streamItems, key), streamItems.length - 1)];
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { darken } from 'polished';
|
||||
import * as React from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { TextScale } from '../../../../common/log_text_scale';
|
||||
import { tintOrShade } from '../../../utils/styles';
|
||||
import { LogTextStreamItemField } from './item_field';
|
||||
|
||||
interface LogTextStreamItemDateFieldProps {
|
||||
children: string;
|
||||
hasHighlights: boolean;
|
||||
isHovered: boolean;
|
||||
scale: TextScale;
|
||||
}
|
||||
|
||||
export class LogTextStreamItemDateField extends React.PureComponent<
|
||||
LogTextStreamItemDateFieldProps,
|
||||
{}
|
||||
> {
|
||||
public render() {
|
||||
const { children, hasHighlights, isHovered, scale } = this.props;
|
||||
|
||||
return (
|
||||
<LogTextStreamItemDateFieldWrapper
|
||||
hasHighlights={hasHighlights}
|
||||
isHovered={isHovered}
|
||||
scale={scale}
|
||||
>
|
||||
{children}
|
||||
</LogTextStreamItemDateFieldWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const highlightedFieldStyle = css`
|
||||
background-color: ${props =>
|
||||
tintOrShade(props.theme.eui.euiTextColor, props.theme.eui.euiColorSecondary, 0.15)};
|
||||
border-color: ${props => props.theme.eui.euiColorSecondary};
|
||||
`;
|
||||
|
||||
const hoveredFieldStyle = css`
|
||||
background-color: ${props => darken(0.05, props.theme.eui.euiColorHighlight)};
|
||||
border-color: ${props => darken(0.2, props.theme.eui.euiColorHighlight)};
|
||||
color: ${props => props.theme.eui.euiColorFullShade};
|
||||
`;
|
||||
|
||||
const LogTextStreamItemDateFieldWrapper = LogTextStreamItemField.extend.attrs<{
|
||||
hasHighlights: boolean;
|
||||
isHovered: boolean;
|
||||
}>({})`
|
||||
background-color: ${props => props.theme.eui.euiColorLightestShade};
|
||||
border-right: solid 2px ${props => props.theme.eui.euiColorLightShade};
|
||||
color: ${props => props.theme.eui.euiColorDarkShade};
|
||||
white-space: pre;
|
||||
|
||||
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
|
||||
${props => (props.isHovered ? hoveredFieldStyle : '')};
|
||||
`;
|
|
@ -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 styled from 'styled-components';
|
||||
|
||||
import { switchProp } from '../../../utils/styles';
|
||||
|
||||
export const LogTextStreamItemField = styled.div.attrs<{
|
||||
scale?: 'small' | 'medium' | 'large';
|
||||
}>({})`
|
||||
font-size: ${props =>
|
||||
switchProp('scale', {
|
||||
large: props.theme.eui.euiFontSizeM,
|
||||
medium: props.theme.eui.euiFontSizeS,
|
||||
small: props.theme.eui.euiFontSizeXs,
|
||||
[switchProp.default]: props.theme.eui.euiFontSize,
|
||||
})};
|
||||
line-height: ${props => props.theme.eui.euiLineHeight};
|
||||
padding: 2px ${props => props.theme.eui.euiSize};
|
||||
`;
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { darken } from 'polished';
|
||||
import * as React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { TextScale } from '../../../../common/log_text_scale';
|
||||
import { tintOrShade } from '../../../utils/styles';
|
||||
import { LogTextStreamItemField } from './item_field';
|
||||
|
||||
interface LogTextStreamItemMessageFieldProps {
|
||||
children: string;
|
||||
highlights: string[];
|
||||
isHovered: boolean;
|
||||
isWrapped: boolean;
|
||||
scale: TextScale;
|
||||
}
|
||||
|
||||
export class LogTextStreamItemMessageField extends React.PureComponent<
|
||||
LogTextStreamItemMessageFieldProps,
|
||||
{}
|
||||
> {
|
||||
public render() {
|
||||
const { children, highlights, isHovered, isWrapped, scale } = this.props;
|
||||
|
||||
const hasHighlights = highlights.length > 0;
|
||||
const content = hasHighlights ? renderHighlightFragments(children, highlights) : children;
|
||||
return (
|
||||
<LogTextStreamItemMessageFieldWrapper
|
||||
hasHighlights={hasHighlights}
|
||||
isHovered={isHovered}
|
||||
isWrapped={isWrapped}
|
||||
scale={scale}
|
||||
>
|
||||
{content}
|
||||
</LogTextStreamItemMessageFieldWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderHighlightFragments = (text: string, highlights: string[]): React.ReactNode[] => {
|
||||
const renderedHighlights = highlights.reduce(
|
||||
({ lastFragmentEnd, renderedFragments }, highlight) => {
|
||||
const fragmentStart = text.indexOf(highlight, lastFragmentEnd);
|
||||
return {
|
||||
lastFragmentEnd: fragmentStart + highlight.length,
|
||||
renderedFragments: [
|
||||
...renderedFragments,
|
||||
text.slice(lastFragmentEnd, fragmentStart),
|
||||
<HighlightSpan key={fragmentStart}>{highlight}</HighlightSpan>,
|
||||
],
|
||||
};
|
||||
},
|
||||
{
|
||||
lastFragmentEnd: 0,
|
||||
renderedFragments: [],
|
||||
} as {
|
||||
lastFragmentEnd: number;
|
||||
renderedFragments: React.ReactNode[];
|
||||
}
|
||||
);
|
||||
|
||||
return [...renderedHighlights.renderedFragments, text.slice(renderedHighlights.lastFragmentEnd)];
|
||||
};
|
||||
|
||||
const highlightedFieldStyle = css`
|
||||
background-color: ${props =>
|
||||
tintOrShade(props.theme.eui.euiTextColor, props.theme.eui.euiColorSecondary, 0.15)};
|
||||
`;
|
||||
|
||||
const hoveredFieldStyle = css`
|
||||
background-color: ${props => darken(0.05, props.theme.eui.euiColorHighlight)};
|
||||
`;
|
||||
|
||||
const wrappedFieldStyle = css`
|
||||
overflow: visible;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
const unwrappedFieldStyle = css`
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
`;
|
||||
|
||||
const LogTextStreamItemMessageFieldWrapper = LogTextStreamItemField.extend.attrs<{
|
||||
hasHighlights: boolean;
|
||||
isHovered: boolean;
|
||||
isWrapped?: boolean;
|
||||
}>({})`
|
||||
flex-grow: 1;
|
||||
text-overflow: ellipsis;
|
||||
background-color: ${props => props.theme.eui.euiColorEmptyShade};
|
||||
|
||||
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
|
||||
${props => (props.isHovered ? hoveredFieldStyle : '')};
|
||||
${props => (props.isWrapped ? wrappedFieldStyle : unwrappedFieldStyle)};
|
||||
`;
|
||||
|
||||
const HighlightSpan = styled.span`
|
||||
display: inline-block;
|
||||
padding: 0 ${props => props.theme.eui.euiSizeXs};
|
||||
background-color: ${props => props.theme.eui.euiColorSecondary};
|
||||
color: ${props => props.theme.eui.euiColorGhost};
|
||||
font-weight: ${props => props.theme.eui.euiFontWeightMedium};
|
||||
`;
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
|
||||
import { TextScale } from '../../../../common/log_text_scale';
|
||||
import { StreamItem } from './item';
|
||||
import { LogTextStreamLogEntryItemView } from './log_entry_item_view';
|
||||
|
||||
interface StreamItemProps {
|
||||
item: StreamItem;
|
||||
scale: TextScale;
|
||||
wrap: boolean;
|
||||
}
|
||||
|
||||
export const LogTextStreamItemView = React.forwardRef<Element, StreamItemProps>(
|
||||
({ item, scale, wrap }, ref) => {
|
||||
switch (item.kind) {
|
||||
case 'logEntry':
|
||||
return (
|
||||
<LogTextStreamLogEntryItemView
|
||||
boundingBoxRef={ref}
|
||||
logEntry={item.logEntry}
|
||||
searchResult={item.searchResult}
|
||||
scale={scale}
|
||||
wrap={wrap}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiProgress, EuiText } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { RelativeTime } from './relative_time';
|
||||
|
||||
interface LogTextStreamLoadingItemViewProps {
|
||||
alignment: 'top' | 'bottom';
|
||||
className?: string;
|
||||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
isStreaming: boolean;
|
||||
lastStreamingUpdate: number | null;
|
||||
}
|
||||
|
||||
export class LogTextStreamLoadingItemView extends React.PureComponent<
|
||||
LogTextStreamLoadingItemViewProps,
|
||||
{}
|
||||
> {
|
||||
public render() {
|
||||
const {
|
||||
alignment,
|
||||
className,
|
||||
hasMore,
|
||||
isLoading,
|
||||
isStreaming,
|
||||
lastStreamingUpdate,
|
||||
} = this.props;
|
||||
|
||||
if (isStreaming) {
|
||||
return (
|
||||
<ProgressEntry alignment={alignment} className={className} color="primary" isLoading={true}>
|
||||
<EuiText color="subdued">
|
||||
Streaming new entries
|
||||
{lastStreamingUpdate ? (
|
||||
<>
|
||||
: last updated <RelativeTime time={lastStreamingUpdate} refreshInterval={1000} />{' '}
|
||||
ago
|
||||
</>
|
||||
) : null}
|
||||
</EuiText>
|
||||
</ProgressEntry>
|
||||
);
|
||||
} else if (isLoading) {
|
||||
return (
|
||||
<ProgressEntry alignment={alignment} className={className} color="subdued" isLoading={true}>
|
||||
Loading additional entries
|
||||
</ProgressEntry>
|
||||
);
|
||||
} else if (!hasMore) {
|
||||
return (
|
||||
<ProgressEntry
|
||||
alignment={alignment}
|
||||
className={className}
|
||||
color="subdued"
|
||||
isLoading={false}
|
||||
>
|
||||
No additional entries found
|
||||
</ProgressEntry>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ProgressEntryProps {
|
||||
alignment: 'top' | 'bottom';
|
||||
className?: string;
|
||||
color: 'subdued' | 'primary';
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:max-classes-per-file
|
||||
class ProgressEntry extends React.PureComponent<ProgressEntryProps, {}> {
|
||||
public render() {
|
||||
const { alignment, children, className, color, isLoading } = this.props;
|
||||
|
||||
return (
|
||||
<ProgressEntryWrapper className={className}>
|
||||
<AlignedProgress
|
||||
alignment={alignment}
|
||||
color={color}
|
||||
max={isLoading ? undefined : 1}
|
||||
size="xs"
|
||||
value={isLoading ? undefined : 1}
|
||||
position="absolute"
|
||||
/>
|
||||
<ProgressTextDiv>{children}</ProgressTextDiv>
|
||||
</ProgressEntryWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ProgressEntryWrapper = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ProgressTextDiv = styled.div`
|
||||
padding: 8px 16px;
|
||||
`;
|
||||
|
||||
const AlignedProgress = styled(EuiProgress).attrs<{
|
||||
alignment: 'top' | 'bottom';
|
||||
}>({})`
|
||||
top: ${props => (props.alignment === 'top' ? 0 : 'initial')};
|
||||
bottom: ${props => (props.alignment === 'top' ? 'initial' : 0)};
|
||||
`;
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { LogEntry } from '../../../../common/log_entry';
|
||||
import { SearchResult } from '../../../../common/log_search_result';
|
||||
import { TextScale } from '../../../../common/log_text_scale';
|
||||
import { formatTime } from '../../../../common/time';
|
||||
import { LogTextStreamItemDateField } from './item_date_field';
|
||||
import { LogTextStreamItemMessageField } from './item_message_field';
|
||||
|
||||
interface LogTextStreamLogEntryItemViewProps {
|
||||
boundingBoxRef?: React.Ref<Element>;
|
||||
logEntry: LogEntry;
|
||||
searchResult?: SearchResult;
|
||||
scale: TextScale;
|
||||
wrap: boolean;
|
||||
}
|
||||
|
||||
interface LogTextStreamLogEntryItemViewState {
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
export class LogTextStreamLogEntryItemView extends React.PureComponent<
|
||||
LogTextStreamLogEntryItemViewProps,
|
||||
LogTextStreamLogEntryItemViewState
|
||||
> {
|
||||
public readonly state = {
|
||||
isHovered: false,
|
||||
};
|
||||
|
||||
public handleMouseEnter: React.MouseEventHandler<HTMLDivElement> = () => {
|
||||
this.setState({
|
||||
isHovered: true,
|
||||
});
|
||||
};
|
||||
|
||||
public handleMouseLeave: React.MouseEventHandler<HTMLDivElement> = () => {
|
||||
this.setState({
|
||||
isHovered: false,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { boundingBoxRef, logEntry, scale, searchResult, wrap } = this.props;
|
||||
const { isHovered } = this.state;
|
||||
|
||||
return (
|
||||
<LogTextStreamLogEntryItemDiv
|
||||
innerRef={
|
||||
/* Workaround for missing RefObject support in styled-components */
|
||||
boundingBoxRef as any
|
||||
}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<LogTextStreamItemDateField
|
||||
hasHighlights={!!searchResult}
|
||||
isHovered={isHovered}
|
||||
scale={scale}
|
||||
>
|
||||
{formatTime(logEntry.fields.time)}
|
||||
</LogTextStreamItemDateField>
|
||||
<LogTextStreamItemMessageField
|
||||
highlights={searchResult ? searchResult.matches.message || [] : []}
|
||||
isHovered={isHovered}
|
||||
isWrapped={wrap}
|
||||
scale={scale}
|
||||
>
|
||||
{logEntry.fields.message}
|
||||
</LogTextStreamItemMessageField>
|
||||
</LogTextStreamLogEntryItemDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LogTextStreamLogEntryItemDiv = styled.div`
|
||||
font-family: ${props => props.theme.eui.euiCodeFontFamily};
|
||||
font-size: ${props => props.theme.eui.euiFontSize};
|
||||
line-height: ${props => props.theme.eui.euiLineHeight};
|
||||
color: ${props => props.theme.eui.euiTextColor};
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
`;
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { LogEntry } from '../../../../common/log_entry';
|
||||
|
||||
interface LogEntryStreamItemViewProps {
|
||||
boundingBoxRef?: React.Ref<{}>;
|
||||
item: LogEntry;
|
||||
}
|
||||
|
||||
export class LogEntryStreamItemView extends React.PureComponent<LogEntryStreamItemViewProps, {}> {
|
||||
public render() {
|
||||
const { boundingBoxRef, item } = this.props;
|
||||
|
||||
return (
|
||||
// @ts-ignore: silence error until styled-components supports React.RefObject<T>
|
||||
<LogEntryDiv innerRef={boundingBoxRef}>{JSON.stringify(item)}</LogEntryDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LogEntryDiv = styled.div`
|
||||
border-top: 1px solid red;
|
||||
border-bottom: 1px solid green;
|
||||
`;
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
|
||||
export interface Rect {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface MeasureableProps {
|
||||
children: (measureRef: React.Ref<HTMLElement>) => React.ReactNode;
|
||||
register: (key: any, element: MeasurableItemView | null) => void;
|
||||
registrationKey: any;
|
||||
}
|
||||
|
||||
export class MeasurableItemView extends React.PureComponent<MeasureableProps, {}> {
|
||||
public childRef = React.createRef<HTMLElement>();
|
||||
|
||||
public getOffsetRect = (): Rect | null => {
|
||||
const currentElement = this.childRef.current;
|
||||
|
||||
if (currentElement === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
height: currentElement.offsetHeight,
|
||||
left: currentElement.offsetLeft,
|
||||
top: currentElement.offsetTop,
|
||||
width: currentElement.offsetWidth,
|
||||
};
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.register(this.props.registrationKey, this);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.props.register(this.props.registrationKey, null);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: MeasureableProps) {
|
||||
if (prevProps.registrationKey !== this.props.registrationKey) {
|
||||
this.props.register(prevProps.registrationKey, null);
|
||||
this.props.register(this.props.registrationKey, this);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.props.children(this.childRef);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
|
||||
import { decomposeIntoUnits, getLabelOfScale, TimeUnit } from '../../../../common/time';
|
||||
|
||||
interface RelativeTimeProps {
|
||||
time: number;
|
||||
refreshInterval?: number;
|
||||
}
|
||||
|
||||
interface RelativeTimeState {
|
||||
currentTime: number;
|
||||
timeoutId: number | null;
|
||||
}
|
||||
|
||||
export class RelativeTime extends React.Component<RelativeTimeProps, RelativeTimeState> {
|
||||
public readonly state = {
|
||||
currentTime: Date.now(),
|
||||
timeoutId: null,
|
||||
};
|
||||
|
||||
public updateCurrentTimeEvery = (refreshInterval: number) => {
|
||||
const nextTimeoutId = window.setTimeout(
|
||||
this.updateCurrentTimeEvery.bind(this, refreshInterval),
|
||||
refreshInterval
|
||||
);
|
||||
|
||||
this.setState({
|
||||
currentTime: Date.now(),
|
||||
timeoutId: nextTimeoutId,
|
||||
});
|
||||
};
|
||||
|
||||
public cancelUpdate = () => {
|
||||
const { timeoutId } = this.state;
|
||||
|
||||
if (timeoutId) {
|
||||
window.clearTimeout(timeoutId);
|
||||
this.setState({
|
||||
timeoutId: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
const { refreshInterval } = this.props;
|
||||
|
||||
if (refreshInterval && refreshInterval > 0) {
|
||||
this.updateCurrentTimeEvery(refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.cancelUpdate();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { time } = this.props;
|
||||
const { currentTime } = this.state;
|
||||
const timeDifference = Math.abs(currentTime - time);
|
||||
|
||||
const timeFragments = decomposeIntoUnits(timeDifference, unitThresholds);
|
||||
|
||||
if (timeFragments.length === 0) {
|
||||
return '0s';
|
||||
} else {
|
||||
return timeFragments.map(getLabelOfScale).join(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unitThresholds = [TimeUnit.Day, TimeUnit.Hour, TimeUnit.Minute, TimeUnit.Second];
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
|
||||
import { TextScale } from '../../../../common/log_text_scale';
|
||||
import { TimeKey } from '../../../../common/time';
|
||||
import { callWithoutRepeats } from '../../../utils/handlers';
|
||||
import { InfraLoadingPanel } from '../../loading';
|
||||
import { LogTextStreamEmptyView } from './empty_view';
|
||||
import { getStreamItemBeforeTimeKey, getStreamItemId, parseStreamItemId, StreamItem } from './item';
|
||||
import { LogTextStreamItemView } from './item_view';
|
||||
import { LogTextStreamLoadingItemView } from './loading_item_view';
|
||||
import { MeasurableItemView } from './measurable_item_view';
|
||||
import { VerticalScrollPanel } from './vertical_scroll_panel';
|
||||
|
||||
interface ScrollableLogTextStreamViewProps {
|
||||
height: number;
|
||||
width: number;
|
||||
items: StreamItem[];
|
||||
scale: TextScale;
|
||||
wrap: boolean;
|
||||
isReloading: boolean;
|
||||
isLoadingMore: boolean;
|
||||
hasMoreBeforeStart: boolean;
|
||||
hasMoreAfterEnd: boolean;
|
||||
isStreaming: boolean;
|
||||
lastLoadedTime: number | null;
|
||||
target: TimeKey | null;
|
||||
jumpToTarget: (target: TimeKey) => any;
|
||||
reportVisibleInterval: (
|
||||
params: {
|
||||
pagesBeforeStart: number;
|
||||
pagesAfterEnd: number;
|
||||
startKey: TimeKey | null;
|
||||
middleKey: TimeKey | null;
|
||||
endKey: TimeKey | null;
|
||||
}
|
||||
) => any;
|
||||
}
|
||||
|
||||
interface ScrollableLogTextStreamViewState {
|
||||
target: TimeKey | null;
|
||||
targetId: string | null;
|
||||
}
|
||||
|
||||
export class ScrollableLogTextStreamView extends React.PureComponent<
|
||||
ScrollableLogTextStreamViewProps,
|
||||
ScrollableLogTextStreamViewState
|
||||
> {
|
||||
public static getDerivedStateFromProps(
|
||||
nextProps: ScrollableLogTextStreamViewProps,
|
||||
prevState: ScrollableLogTextStreamViewState
|
||||
): Partial<ScrollableLogTextStreamViewState> | null {
|
||||
const hasNewTarget = nextProps.target && nextProps.target !== prevState.target;
|
||||
const hasItems = nextProps.items.length > 0;
|
||||
|
||||
if (nextProps.isStreaming && hasItems) {
|
||||
return {
|
||||
target: nextProps.target,
|
||||
targetId: getStreamItemId(nextProps.items[nextProps.items.length - 1]),
|
||||
};
|
||||
} else if (hasNewTarget && hasItems) {
|
||||
return {
|
||||
target: nextProps.target,
|
||||
targetId: getStreamItemId(getStreamItemBeforeTimeKey(nextProps.items, nextProps.target!)),
|
||||
};
|
||||
} else if (!nextProps.target || !hasItems) {
|
||||
return {
|
||||
target: null,
|
||||
targetId: null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public readonly state = {
|
||||
target: null,
|
||||
targetId: null,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const {
|
||||
items,
|
||||
height,
|
||||
width,
|
||||
scale,
|
||||
wrap,
|
||||
isReloading,
|
||||
isLoadingMore,
|
||||
hasMoreBeforeStart,
|
||||
hasMoreAfterEnd,
|
||||
isStreaming,
|
||||
lastLoadedTime,
|
||||
} = this.props;
|
||||
const { targetId } = this.state;
|
||||
const hasItems = items.length > 0;
|
||||
if (isReloading && !hasItems) {
|
||||
return <InfraLoadingPanel height={height} width={width} text="Loading entries" />;
|
||||
} else if (!hasItems) {
|
||||
return <LogTextStreamEmptyView reload={this.handleReload} />;
|
||||
} else {
|
||||
return (
|
||||
<VerticalScrollPanel
|
||||
height={height}
|
||||
width={width}
|
||||
onVisibleChildrenChange={this.handleVisibleChildrenChange}
|
||||
target={targetId}
|
||||
hideScrollbar={true}
|
||||
>
|
||||
{registerChild => (
|
||||
<>
|
||||
<LogTextStreamLoadingItemView
|
||||
alignment="bottom"
|
||||
isLoading={isLoadingMore}
|
||||
hasMore={hasMoreBeforeStart}
|
||||
isStreaming={false}
|
||||
lastStreamingUpdate={null}
|
||||
/>
|
||||
{items.map(item => (
|
||||
<MeasurableItemView
|
||||
register={registerChild}
|
||||
registrationKey={getStreamItemId(item)}
|
||||
key={getStreamItemId(item)}
|
||||
>
|
||||
{measureRef => (
|
||||
<LogTextStreamItemView ref={measureRef} item={item} scale={scale} wrap={wrap} />
|
||||
)}
|
||||
</MeasurableItemView>
|
||||
))}
|
||||
<LogTextStreamLoadingItemView
|
||||
alignment="top"
|
||||
isLoading={isStreaming || isLoadingMore}
|
||||
hasMore={hasMoreAfterEnd}
|
||||
isStreaming={isStreaming}
|
||||
lastStreamingUpdate={isStreaming ? lastLoadedTime : null}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</VerticalScrollPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleReload = () => {
|
||||
const { jumpToTarget, target } = this.props;
|
||||
|
||||
if (target) {
|
||||
jumpToTarget(target);
|
||||
}
|
||||
};
|
||||
|
||||
// this is actually a method but not recognized as such
|
||||
// tslint:disable-next-line:member-ordering
|
||||
private handleVisibleChildrenChange = callWithoutRepeats(
|
||||
({
|
||||
topChild,
|
||||
middleChild,
|
||||
bottomChild,
|
||||
pagesAbove,
|
||||
pagesBelow,
|
||||
}: {
|
||||
topChild: string;
|
||||
middleChild: string;
|
||||
bottomChild: string;
|
||||
pagesAbove: number;
|
||||
pagesBelow: number;
|
||||
}) => {
|
||||
this.props.reportVisibleInterval({
|
||||
endKey: parseStreamItemId(bottomChild),
|
||||
middleKey: parseStreamItemId(middleChild),
|
||||
pagesAfterEnd: pagesBelow,
|
||||
pagesBeforeStart: pagesAbove,
|
||||
startKey: parseStreamItemId(topChild),
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* 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 { bisector } from 'd3-array';
|
||||
import sortBy from 'lodash/fp/sortBy';
|
||||
import throttle from 'lodash/fp/throttle';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Rect } from './measurable_item_view';
|
||||
|
||||
interface VerticalScrollPanelProps<Child> {
|
||||
children?: (
|
||||
registerChild: (key: Child, element: MeasurableChild | null) => void
|
||||
) => React.ReactNode;
|
||||
onVisibleChildrenChange?: (
|
||||
visibleChildren: {
|
||||
topChild: Child;
|
||||
middleChild: Child;
|
||||
bottomChild: Child;
|
||||
pagesAbove: number;
|
||||
pagesBelow: number;
|
||||
}
|
||||
) => void;
|
||||
target: Child | undefined;
|
||||
height: number;
|
||||
width: number;
|
||||
hideScrollbar?: boolean;
|
||||
}
|
||||
|
||||
interface VerticalScrollPanelSnapshot<Child> {
|
||||
scrollTarget: Child | undefined;
|
||||
scrollOffset: number | undefined;
|
||||
}
|
||||
|
||||
interface MeasurableChild {
|
||||
getOffsetRect(): Rect | null;
|
||||
}
|
||||
|
||||
const SCROLL_THROTTLE_INTERVAL = 250;
|
||||
const ASSUMED_SCROLLBAR_WIDTH = 20;
|
||||
|
||||
export class VerticalScrollPanel<Child> extends React.PureComponent<
|
||||
VerticalScrollPanelProps<Child>
|
||||
> {
|
||||
public static defaultProps: Partial<VerticalScrollPanelProps<any>> = {
|
||||
hideScrollbar: false,
|
||||
};
|
||||
|
||||
public scrollRef = React.createRef<HTMLDivElement>();
|
||||
public childRefs = new Map<Child, MeasurableChild>();
|
||||
public childDimensions = new Map<Child, Rect>();
|
||||
|
||||
public handleScroll: React.UIEventHandler<HTMLDivElement> = throttle(
|
||||
SCROLL_THROTTLE_INTERVAL,
|
||||
() => {
|
||||
this.reportVisibleChildren();
|
||||
}
|
||||
);
|
||||
|
||||
public registerChild = (key: any, element: MeasurableChild | null) => {
|
||||
if (element === null) {
|
||||
this.childRefs.delete(key);
|
||||
} else {
|
||||
this.childRefs.set(key, element);
|
||||
}
|
||||
};
|
||||
|
||||
public updateChildDimensions = () => {
|
||||
this.childDimensions = new Map<Child, Rect>(
|
||||
sortDimensionsByTop(
|
||||
Array.from(this.childRefs.entries()).reduce(
|
||||
(accumulatedDimensions, [key, child]) => {
|
||||
const currentOffsetRect = child.getOffsetRect();
|
||||
|
||||
if (currentOffsetRect !== null) {
|
||||
accumulatedDimensions.push([key, currentOffsetRect]);
|
||||
}
|
||||
|
||||
return accumulatedDimensions;
|
||||
},
|
||||
[] as Array<[any, Rect]>
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
public getVisibleChildren = () => {
|
||||
if (this.scrollRef.current === null || this.childDimensions.size <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
childDimensions,
|
||||
props: { height: scrollViewHeight },
|
||||
scrollRef: {
|
||||
current: { scrollTop },
|
||||
},
|
||||
} = this;
|
||||
|
||||
return getVisibleChildren(Array.from(childDimensions.entries()), scrollViewHeight, scrollTop);
|
||||
};
|
||||
|
||||
public getScrollPosition = () => {
|
||||
if (this.scrollRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
props: { height: scrollViewHeight },
|
||||
scrollRef: {
|
||||
current: { scrollHeight, scrollTop },
|
||||
},
|
||||
} = this;
|
||||
|
||||
return {
|
||||
pagesAbove: scrollTop / scrollViewHeight,
|
||||
pagesBelow: (scrollHeight - scrollTop - scrollViewHeight) / scrollViewHeight,
|
||||
};
|
||||
};
|
||||
|
||||
public reportVisibleChildren = () => {
|
||||
const { onVisibleChildrenChange } = this.props;
|
||||
const visibleChildren = this.getVisibleChildren();
|
||||
const scrollPosition = this.getScrollPosition();
|
||||
|
||||
if (!visibleChildren || !scrollPosition || typeof onVisibleChildrenChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
onVisibleChildrenChange({
|
||||
bottomChild: visibleChildren.bottomChild,
|
||||
middleChild: visibleChildren.middleChild,
|
||||
topChild: visibleChildren.topChild,
|
||||
...scrollPosition,
|
||||
});
|
||||
};
|
||||
|
||||
public centerTarget = (target: Child, offset?: number) => {
|
||||
const {
|
||||
props: { height: scrollViewHeight },
|
||||
childDimensions,
|
||||
scrollRef,
|
||||
} = this;
|
||||
|
||||
if (scrollRef.current === null || !target || childDimensions.size <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetDimensions = childDimensions.get(target);
|
||||
|
||||
if (targetDimensions) {
|
||||
const targetOffset = typeof offset === 'undefined' ? targetDimensions.height / 2 : offset;
|
||||
scrollRef.current.scrollTop = targetDimensions.top + targetOffset - scrollViewHeight / 2;
|
||||
}
|
||||
};
|
||||
|
||||
public handleUpdatedChildren = (target: Child | undefined, offset: number | undefined) => {
|
||||
this.updateChildDimensions();
|
||||
if (!!target) {
|
||||
this.centerTarget(target, offset);
|
||||
}
|
||||
this.reportVisibleChildren();
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.handleUpdatedChildren(this.props.target, undefined);
|
||||
}
|
||||
|
||||
public getSnapshotBeforeUpdate(
|
||||
prevProps: VerticalScrollPanelProps<Child>
|
||||
): VerticalScrollPanelSnapshot<Child> {
|
||||
if (prevProps.target !== this.props.target && this.props.target) {
|
||||
return {
|
||||
scrollOffset: undefined,
|
||||
scrollTarget: this.props.target,
|
||||
};
|
||||
} else {
|
||||
const visibleChildren = this.getVisibleChildren();
|
||||
|
||||
if (visibleChildren) {
|
||||
return {
|
||||
scrollOffset: visibleChildren.middleChildOffset,
|
||||
scrollTarget: visibleChildren.middleChild,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scrollOffset: undefined,
|
||||
scrollTarget: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidUpdate(
|
||||
prevProps: VerticalScrollPanelProps<Child>,
|
||||
prevState: {},
|
||||
snapshot: VerticalScrollPanelSnapshot<Child>
|
||||
) {
|
||||
this.handleUpdatedChildren(snapshot.scrollTarget, snapshot.scrollOffset);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.childRefs.clear();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { children, height, width, hideScrollbar } = this.props;
|
||||
const scrollbarOffset = hideScrollbar ? ASSUMED_SCROLLBAR_WIDTH : 0;
|
||||
|
||||
return (
|
||||
<ScrollPanelWrapper
|
||||
style={{ height, width: width + scrollbarOffset }}
|
||||
scrollbarOffset={scrollbarOffset}
|
||||
onScroll={this.handleScroll}
|
||||
innerRef={
|
||||
/* workaround for missing RefObject support in styled-components typings */
|
||||
this.scrollRef as any
|
||||
}
|
||||
>
|
||||
{typeof children === 'function' ? children(this.registerChild) : null}
|
||||
</ScrollPanelWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ScrollPanelWrapper = styled.div.attrs<{ scrollbarOffset?: number }>({})`
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
position: relative;
|
||||
padding-right: ${props => props.scrollbarOffset || 0}px;
|
||||
|
||||
& * {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const getVisibleChildren = <Child extends {}>(
|
||||
childDimensions: Array<[Child, Rect]>,
|
||||
scrollViewHeight: number,
|
||||
scrollTop: number
|
||||
) => {
|
||||
const middleChildIndex = Math.min(
|
||||
getChildIndexBefore(childDimensions, scrollTop + scrollViewHeight / 2),
|
||||
childDimensions.length - 1
|
||||
);
|
||||
|
||||
const topChildIndex = Math.min(
|
||||
getChildIndexBefore(childDimensions, scrollTop, 0, middleChildIndex),
|
||||
childDimensions.length - 1
|
||||
);
|
||||
|
||||
const bottomChildIndex = Math.min(
|
||||
getChildIndexBefore(childDimensions, scrollTop + scrollViewHeight, middleChildIndex),
|
||||
childDimensions.length - 1
|
||||
);
|
||||
|
||||
return {
|
||||
bottomChild: childDimensions[bottomChildIndex][0],
|
||||
bottomChildOffset: childDimensions[bottomChildIndex][1].top - scrollTop - scrollViewHeight,
|
||||
middleChild: childDimensions[middleChildIndex][0],
|
||||
middleChildOffset: scrollTop + scrollViewHeight / 2 - childDimensions[middleChildIndex][1].top,
|
||||
topChild: childDimensions[topChildIndex][0],
|
||||
topChildOffset: childDimensions[topChildIndex][1].top - scrollTop,
|
||||
};
|
||||
};
|
||||
|
||||
const sortDimensionsByTop = sortBy<[any, Rect]>('1.top');
|
||||
|
||||
const getChildIndexBefore = bisector<[any, Rect], number>(([key, rect]) => rect.top + rect.height)
|
||||
.left;
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
|
||||
interface LogTextWrapControlsProps {
|
||||
wrap: boolean;
|
||||
setTextWrap: (scale: boolean) => any;
|
||||
}
|
||||
|
||||
export class LogTextWrapControls extends React.PureComponent<LogTextWrapControlsProps> {
|
||||
public toggleWrap = () => {
|
||||
this.props.setTextWrap(!this.props.wrap);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { wrap } = this.props;
|
||||
|
||||
return (
|
||||
<EuiFormRow label="Line Wrapping">
|
||||
<EuiSwitch label="Wrap long lines" checked={wrap} onChange={this.toggleWrap} />
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { EuiDatePicker, EuiFilterButton, EuiFilterGroup } from '@elastic/eui';
|
||||
import moment, { Moment } from 'moment';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const noop = () => undefined;
|
||||
|
||||
interface LogTimeControlsProps {
|
||||
currentTime: number | null;
|
||||
startLiveStreaming: (interval: number) => any;
|
||||
stopLiveStreaming: () => any;
|
||||
isLiveStreaming: boolean;
|
||||
jumpToTime: (time: number) => any;
|
||||
}
|
||||
|
||||
export class LogTimeControls extends React.PureComponent<LogTimeControlsProps> {
|
||||
public render() {
|
||||
const { currentTime, isLiveStreaming } = this.props;
|
||||
|
||||
const currentMoment = currentTime ? moment(currentTime) : null;
|
||||
|
||||
if (isLiveStreaming) {
|
||||
return (
|
||||
<EuiFilterGroup>
|
||||
<InlineWrapper>
|
||||
<EuiDatePicker disabled onChange={noop} value="streaming..." />
|
||||
</InlineWrapper>
|
||||
<EuiFilterButton
|
||||
color="primary"
|
||||
iconType="pause"
|
||||
iconSide="left"
|
||||
onClick={this.stopLiveStreaming}
|
||||
>
|
||||
Stop streaming
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EuiFilterGroup>
|
||||
<InlineWrapper>
|
||||
<EuiDatePicker
|
||||
dateFormat="L LTS"
|
||||
onChange={this.handleChangeDate}
|
||||
popperPlacement="top-end"
|
||||
selected={currentMoment}
|
||||
shouldCloseOnSelect
|
||||
showTimeSelect
|
||||
timeFormat="LTS"
|
||||
injectTimes={currentMoment ? [currentMoment] : []}
|
||||
/>
|
||||
</InlineWrapper>
|
||||
<EuiFilterButton iconType="play" iconSide="left" onClick={this.startLiveStreaming}>
|
||||
Stream live
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleChangeDate = (date: Moment | null) => {
|
||||
if (date !== null) {
|
||||
this.props.jumpToTime(date.valueOf());
|
||||
}
|
||||
};
|
||||
|
||||
private startLiveStreaming = () => {
|
||||
this.props.startLiveStreaming(5000);
|
||||
};
|
||||
|
||||
private stopLiveStreaming = () => {
|
||||
this.props.stopLiveStreaming();
|
||||
};
|
||||
}
|
||||
|
||||
const InlineWrapper = styled.div`
|
||||
display: inline-block;
|
||||
`;
|
84
x-pack/plugins/infra/public/components/metrics/index.tsx
Normal file
84
x-pack/plugins/infra/public/components/metrics/index.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { EuiPageContentBody, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { InfraMetricData } from '../../../common/graphql/types';
|
||||
import { InfraMetricLayout, InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
|
||||
import { metricTimeActions } from '../../store';
|
||||
import { InfraLoadingPanel } from '../loading';
|
||||
import { Section } from './section';
|
||||
|
||||
interface Props {
|
||||
metrics: InfraMetricData[];
|
||||
layouts: InfraMetricLayout[];
|
||||
loading: boolean;
|
||||
nodeName: string;
|
||||
onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
crosshairValue: number | null;
|
||||
}
|
||||
|
||||
export class Metrics extends React.PureComponent<Props, State> {
|
||||
public readonly state = {
|
||||
crosshairValue: null,
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (this.props.loading) {
|
||||
return (
|
||||
<InfraLoadingPanel
|
||||
height="100vh"
|
||||
width="auto"
|
||||
text={`Loading data for ${this.props.nodeName}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <React.Fragment>{this.props.layouts.map(this.renderLayout)}</React.Fragment>;
|
||||
}
|
||||
|
||||
private renderLayout = (layout: InfraMetricLayout) => {
|
||||
return (
|
||||
<React.Fragment key={layout.id}>
|
||||
<EuiPageContentBody>
|
||||
<EuiTitle size="m">
|
||||
<h2 id={layout.id}>{`${layout.label} Overview`}</h2>
|
||||
</EuiTitle>
|
||||
</EuiPageContentBody>
|
||||
{layout.sections.map(this.renderSection(layout))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
private renderSection = (layout: InfraMetricLayout) => (section: InfraMetricLayoutSection) => {
|
||||
let sectionProps = {};
|
||||
if (section.type === 'chart') {
|
||||
const { onChangeRangeTime } = this.props;
|
||||
sectionProps = {
|
||||
onChangeRangeTime,
|
||||
crosshairValue: this.state.crosshairValue,
|
||||
onCrosshairUpdate: this.onCrosshairUpdate,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Section
|
||||
section={section}
|
||||
metrics={this.props.metrics}
|
||||
key={`${layout.id}-${section.id}`}
|
||||
{...sectionProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
private onCrosshairUpdate = (crosshairValue: number) => {
|
||||
this.setState({
|
||||
crosshairValue,
|
||||
});
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue