Typescript search embeddable (#26863) (#27052)

* typescript search embeddable

* Add a comment
This commit is contained in:
Stacey Gammon 2018-12-12 14:46:09 -05:00 committed by GitHub
parent b111074004
commit 6211eb2b1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 376 additions and 229 deletions

View file

@ -32,11 +32,16 @@ declare const datemath: {
unitsAsc: Unit[];
unitsDesc: Unit[];
/**
* Parses a string into a moment object. The string can be something like "now - 15m".
* @param options.forceNow If this optional parameter is supplied, "now" will be treated as this
* date, rather than the real "now".
*/
parse(
input: string,
options?: {
roundUp?: boolean;
forceNow?: boolean;
forceNow?: Date;
momentInstance?: typeof moment;
}
): moment.Moment | undefined;

View file

@ -1,171 +0,0 @@
/*
* 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 angular from 'angular';
import { Embeddable } from 'ui/embeddable';
import searchTemplate from './search_template.html';
import * as columnActions from 'ui/doc_table/actions/columns';
import { getTime } from 'ui/timefilter/get_time';
import { RequestAdapter } from 'ui/inspector/adapters';
export class SearchEmbeddable extends Embeddable {
constructor({ onEmbeddableStateChanged, savedSearch, editUrl, loader, $rootScope, $compile }) {
super({
metadata: {
title: savedSearch.title,
editUrl,
indexPattern: savedSearch.searchSource.getField('index')
}
});
this.onEmbeddableStateChanged = onEmbeddableStateChanged;
this.savedSearch = savedSearch;
this.loader = loader;
this.$rootScope = $rootScope;
this.$compile = $compile;
this.customization = {};
this.inspectorAdaptors = {
requests: new RequestAdapter()
};
}
getInspectorAdapters() {
return this.inspectorAdaptors;
}
emitEmbeddableStateChange(embeddableState) {
this.onEmbeddableStateChanged(embeddableState);
}
getEmbeddableState() {
return {
customization: this.customization
};
}
pushContainerStateParamsToScope() {
// If there is column or sort data on the panel, that means the original columns or sort settings have
// been overridden in a dashboard.
this.searchScope.columns = this.customization.columns || this.savedSearch.columns;
this.searchScope.sort = this.customization.sort || this.savedSearch.sort;
this.searchScope.sharedItemTitle = this.panelTitle;
this.filtersSearchSource.setField('filter', this.filters);
this.filtersSearchSource.setField('query', this.query);
}
onContainerStateChanged(containerState) {
this.customization = containerState.embeddableCustomization || {};
this.filters = containerState.filters;
this.query = containerState.query;
this.timeRange = containerState.timeRange;
this.panelTitle = '';
if (!containerState.hidePanelTitles) {
this.panelTitle = containerState.customTitle !== undefined ?
containerState.customTitle :
this.savedSearch.title;
}
if (this.searchScope) {
this.pushContainerStateParamsToScope();
}
}
initializeSearchScope() {
this.searchScope = this.$rootScope.$new();
this.searchScope.description = this.savedSearch.description;
this.searchScope.searchSource = this.savedSearch.searchSource;
this.searchScope.inspectorAdapters = this.inspectorAdaptors;
const timeRangeSearchSource = this.searchScope.searchSource.create();
timeRangeSearchSource.setField('filter', () => {
return getTime(this.searchScope.searchSource.getField('index'), this.timeRange);
});
this.filtersSearchSource = this.searchScope.searchSource.create();
this.filtersSearchSource.setParent(timeRangeSearchSource);
this.searchScope.searchSource.setParent(this.filtersSearchSource);
this.pushContainerStateParamsToScope();
this.searchScope.setSortOrder = (columnName, direction) => {
this.searchScope.sort = this.customization.sort = [columnName, direction];
this.emitEmbeddableStateChange(this.getEmbeddableState());
};
this.searchScope.addColumn = (columnName) => {
this.savedSearch.searchSource.getField('index').popularizeField(columnName, 1);
columnActions.addColumn(this.searchScope.columns, columnName);
this.searchScope.columns = this.customization.columns = this.searchScope.columns;
this.emitEmbeddableStateChange(this.getEmbeddableState());
};
this.searchScope.removeColumn = (columnName) => {
this.savedSearch.searchSource.getField('index').popularizeField(columnName, 1);
columnActions.removeColumn(this.searchScope.columns, columnName);
this.customization.columns = this.searchScope.columns;
this.emitEmbeddableStateChange(this.getEmbeddableState());
};
this.searchScope.moveColumn = (columnName, newIndex) => {
columnActions.moveColumn(this.searchScope.columns, columnName, newIndex);
this.customization.columns = this.searchScope.columns;
this.emitEmbeddableStateChange(this.getEmbeddableState());
};
this.searchScope.filter = (field, value, operator) => {
const index = this.savedSearch.searchSource.getField('index').id;
const stagedFilter = {
field,
value,
operator,
index
};
this.emitEmbeddableStateChange({
...this.getEmbeddableState(),
stagedFilter,
});
};
}
/**
*
* @param {Element} domNode
* @param {ContainerState} containerState
*/
render(domNode, containerState) {
this.onContainerStateChanged(containerState);
this.domNode = domNode;
this.initializeSearchScope();
this.searchInstance = this.$compile(searchTemplate)(this.searchScope);
const rootNode = angular.element(domNode);
rootNode.append(this.searchInstance);
}
destroy() {
this.savedSearch.destroy();
if (this.searchScope) {
this.searchInstance.remove();
this.searchScope.$destroy();
delete this.searchScope;
}
}
}

View file

@ -0,0 +1,247 @@
/*
* 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 angular from 'angular';
import { SearchSource } from 'ui/courier';
import * as columnActions from 'ui/doc_table/actions/columns';
import {
ContainerState,
Embeddable,
EmbeddableState,
OnEmbeddableStateChanged,
TimeRange,
} from 'ui/embeddable';
import { Filters, Query } from 'ui/embeddable/types';
import { RequestAdapter } from 'ui/inspector/adapters';
import { Adapters } from 'ui/inspector/types';
import { getTime } from 'ui/timefilter/get_time';
import { SavedSearch } from '../types';
import searchTemplate from './search_template.html';
interface SearchScope extends ng.IScope {
columns?: string[];
description?: string;
sort?: string[];
searchSource?: SearchSource;
sharedItemTitle?: string;
inspectorAdapters?: Adapters;
setSortOrder?: (column: string, columnDirection: string) => void;
removeColumn?: (column: string) => void;
addColumn?: (column: string) => void;
moveColumn?: (column: string, index: number) => void;
filter?: (field: string, value: string, operator: string) => void;
}
interface SearchEmbeddableCustomization {
sort?: string[];
columns?: string[];
}
interface SearchEmbeddableConfig {
onEmbeddableStateChanged: OnEmbeddableStateChanged;
savedSearch: SavedSearch;
editUrl: string;
$rootScope: ng.IRootScopeService;
$compile: ng.ICompileService;
}
export class SearchEmbeddable extends Embeddable {
private readonly onEmbeddableStateChanged: OnEmbeddableStateChanged;
private readonly savedSearch: SavedSearch;
private $rootScope: ng.IRootScopeService;
private $compile: ng.ICompileService;
private customization: SearchEmbeddableCustomization;
private inspectorAdaptors: Adapters;
private searchScope?: SearchScope;
private panelTitle: string = '';
private filtersSearchSource: SearchSource;
private timeRange?: TimeRange;
private filters?: Filters;
private query?: Query;
private searchInstance?: JQLite;
constructor({
onEmbeddableStateChanged,
savedSearch,
editUrl,
$rootScope,
$compile,
}: SearchEmbeddableConfig) {
super({
metadata: {
title: savedSearch.title,
editUrl,
indexPattern: savedSearch.searchSource.getField('index'),
},
});
this.onEmbeddableStateChanged = onEmbeddableStateChanged;
this.savedSearch = savedSearch;
this.$rootScope = $rootScope;
this.$compile = $compile;
this.customization = {};
this.inspectorAdaptors = {
requests: new RequestAdapter(),
};
}
public getInspectorAdapters() {
return this.inspectorAdaptors;
}
public onContainerStateChanged(containerState: ContainerState) {
this.customization = containerState.embeddableCustomization || {};
this.filters = containerState.filters;
this.query = containerState.query;
this.timeRange = containerState.timeRange;
this.panelTitle = '';
if (!containerState.hidePanelTitles) {
this.panelTitle =
containerState.customTitle !== undefined
? containerState.customTitle
: this.savedSearch.title;
}
if (this.searchScope) {
this.pushContainerStateParamsToScope(this.searchScope);
}
}
/**
*
* @param {Element} domNode
* @param {ContainerState} containerState
*/
public render(domNode: HTMLElement, containerState: ContainerState) {
this.onContainerStateChanged(containerState);
this.initializeSearchScope();
if (!this.searchScope) {
throw new Error('Search scope not defined');
return;
}
this.searchInstance = this.$compile(searchTemplate)(this.searchScope);
const rootNode = angular.element(domNode);
rootNode.append(this.searchInstance);
}
public destroy() {
this.savedSearch.destroy();
if (this.searchInstance) {
this.searchInstance.remove();
}
if (this.searchScope) {
this.searchScope.$destroy();
delete this.searchScope;
}
}
private initializeSearchScope() {
const searchScope: SearchScope = this.$rootScope.$new();
searchScope.description = this.savedSearch.description;
searchScope.searchSource = this.savedSearch.searchSource;
searchScope.inspectorAdapters = this.inspectorAdaptors;
const timeRangeSearchSource = searchScope.searchSource.create();
timeRangeSearchSource.setField('filter', () => {
if (!this.searchScope || !this.timeRange) {
return;
}
return getTime(this.searchScope.searchSource.getField('index'), this.timeRange);
});
this.filtersSearchSource = searchScope.searchSource.create();
this.filtersSearchSource.setParent(timeRangeSearchSource);
searchScope.searchSource.setParent(this.filtersSearchSource);
this.pushContainerStateParamsToScope(searchScope);
searchScope.setSortOrder = (columnName, direction) => {
searchScope.sort = this.customization.sort = [columnName, direction];
this.emitEmbeddableStateChange(this.getEmbeddableState());
};
searchScope.addColumn = (columnName: string) => {
if (!searchScope.columns) {
return;
}
this.savedSearch.searchSource.getField('index').popularizeField(columnName, 1);
columnActions.addColumn(searchScope.columns, columnName);
searchScope.columns = this.customization.columns = searchScope.columns;
this.emitEmbeddableStateChange(this.getEmbeddableState());
};
searchScope.removeColumn = (columnName: string) => {
if (!searchScope.columns) {
return;
}
this.savedSearch.searchSource.getField('index').popularizeField(columnName, 1);
columnActions.removeColumn(searchScope.columns, columnName);
this.customization.columns = searchScope.columns;
this.emitEmbeddableStateChange(this.getEmbeddableState());
};
searchScope.moveColumn = (columnName, newIndex: number) => {
if (!searchScope.columns) {
return;
}
columnActions.moveColumn(searchScope.columns, columnName, newIndex);
this.customization.columns = searchScope.columns;
this.emitEmbeddableStateChange(this.getEmbeddableState());
};
searchScope.filter = (field, value, operator) => {
const index = this.savedSearch.searchSource.getField('index').id;
const stagedFilter = {
field,
value,
operator,
index,
};
this.emitEmbeddableStateChange({
...this.getEmbeddableState(),
stagedFilter,
});
};
this.searchScope = searchScope;
}
private emitEmbeddableStateChange(embeddableState: EmbeddableState) {
this.onEmbeddableStateChanged(embeddableState);
}
private getEmbeddableState(): EmbeddableState {
return {
customization: this.customization,
};
}
private pushContainerStateParamsToScope(searchScope: SearchScope) {
// If there is column or sort data on the panel, that means the original columns or sort settings have
// been overridden in a dashboard.
searchScope.columns = this.customization.columns || this.savedSearch.columns;
searchScope.sort = this.customization.sort || this.savedSearch.sort;
searchScope.sharedItemTitle = this.panelTitle;
this.filtersSearchSource.setField('filter', this.filters);
this.filtersSearchSource.setField('query', this.query);
}
}

View file

@ -53,7 +53,6 @@ export class SearchEmbeddableFactory extends EmbeddableFactory {
onEmbeddableStateChanged,
savedSearch: savedObject,
editUrl,
loader: this.searchLoader,
$rootScope: this.$rootScope,
$compile: this.$compile,
});

View file

@ -0,0 +1,34 @@
/*
* 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 { SearchSource } from 'ui/courier';
export interface SavedSearch {
readonly id: string;
title: string;
searchSource: SearchSource;
description?: string;
columns: string[];
sort: string[];
destroy: () => void;
}
export interface SavedSearchLoader {
get: (id: string) => SavedSearch;
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
export function addColumn(columns, columnName) {
export function addColumn(columns: string[], columnName: string) {
if (columns.includes(columnName)) {
return;
}
@ -25,7 +25,7 @@ export function addColumn(columns, columnName) {
columns.push(columnName);
}
export function removeColumn(columns, columnName) {
export function removeColumn(columns: string[], columnName: string) {
if (!columns.includes(columnName)) {
return;
}
@ -33,7 +33,7 @@ export function removeColumn(columns, columnName) {
columns.splice(columns.indexOf(columnName), 1);
}
export function moveColumn(columns, columnName, newIndex) {
export function moveColumn(columns: string[], columnName: string, newIndex: number) {
if (newIndex < 0) {
return;
}
@ -46,6 +46,6 @@ export function moveColumn(columns, columnName, newIndex) {
return;
}
columns.splice(columns.indexOf(columnName), 1); // remove at old index
columns.splice(newIndex, 0, columnName); // insert before new index
columns.splice(columns.indexOf(columnName), 1); // remove at old index
columns.splice(newIndex, 0, columnName); // insert before new index
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
export { EmbeddableFactory } from './embeddable_factory';
export { EmbeddableFactory, OnEmbeddableStateChanged } from './embeddable_factory';
export * from './embeddable';
export * from './context_menu_actions';
export { EmbeddableFactoriesRegistryProvider } from './embeddable_factories_registry';

View file

@ -1,50 +0,0 @@
/*
* 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 _ from 'lodash';
import dateMath from '@elastic/datemath';
export function calculateBounds(timeRange, options = {}) {
return {
min: dateMath.parse(timeRange.from, { forceNow: options.forceNow }),
max: dateMath.parse(timeRange.to, { roundUp: true, forceNow: options.forceNow })
};
}
export function getTime(indexPattern, timeRange, forceNow) {
if (!indexPattern) {
//in CI, we sometimes seem to fail here.
return;
}
let filter;
const timefield = indexPattern.timeFieldName && _.find(indexPattern.fields, { name: indexPattern.timeFieldName });
if (timefield) {
const bounds = calculateBounds(timeRange, { forceNow });
filter = { range: {} };
filter.range[timefield.name] = {
gte: bounds.min.valueOf(),
lte: bounds.max.valueOf(),
format: 'epoch_millis'
};
}
return filter;
}

View file

@ -0,0 +1,82 @@
/*
* 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 dateMath from '@elastic/datemath';
import { find } from 'lodash';
import { IndexPattern } from 'ui/index_patterns';
interface CalculateBoundsOptions {
forceNow?: Date;
}
interface TimeRange {
to: string;
from: string;
}
interface RangeFilter {
gte?: number;
lte?: number;
format: string;
}
interface Filter {
range: { [s: string]: RangeFilter };
}
export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOptions = {}) {
return {
min: dateMath.parse(timeRange.from, { forceNow: options.forceNow }),
max: dateMath.parse(timeRange.to, { roundUp: true, forceNow: options.forceNow }),
};
}
export function getTime(
indexPattern: IndexPattern,
timeRange: TimeRange,
forceNow?: Date
): Filter | undefined {
if (!indexPattern) {
// in CI, we sometimes seem to fail here.
return;
}
let filter: Filter;
const timefield: { name: string } | undefined =
indexPattern.timeFieldName && find(indexPattern.fields, { name: indexPattern.timeFieldName });
if (!timefield) {
return;
}
const bounds = calculateBounds(timeRange, { forceNow });
if (!bounds) {
return;
}
filter = { range: {} };
const min = bounds.min ? bounds.min.valueOf() : 0;
const max = bounds.max ? bounds.max.valueOf() : 0;
filter.range[timefield.name] = {
gte: min,
lte: max,
format: 'epoch_millis',
};
return filter;
}

1
typings/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module '*.html' { const template: string; export default template; }