Typescript dashboard app code. (#37527)

* Typescript dashboard app code. Pulls out part of the massive embeddable API PR.

I typscripted this code as part of that PR because it helped me find errors, but really unnecessarily blew up the size of that PR. So pulling out.

* User filter types from kbn-es-query

* Address code review feedback

* Replace require with import
This commit is contained in:
Stacey Gammon 2019-06-06 09:14:02 -04:00 committed by GitHub
parent 2177267ebc
commit 8d78f64391
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 765 additions and 349 deletions

View file

@ -40,7 +40,7 @@ export interface FilterMeta {
export interface Filter {
$state: FilterState;
meta: FilterMeta;
query?: any;
query?: object;
}
export interface LatLon {

View file

@ -17,21 +17,31 @@
* under the License.
*/
import { AppStateClass } from 'ui/state_management/app_state';
/**
* A poor excuse for a mock just to get some basic tests to run in jest without requiring the injector.
* This could be improved if we extract the appState and state classes externally of their angular providers.
* @return {AppStateMock}
*/
export function getAppStateMock() {
export function getAppStateMock(): AppStateClass {
class AppStateMock {
constructor(defaults) {
constructor(defaults: any) {
Object.assign(this, defaults);
}
on() {}
off() {}
toJSON() { return ''; }
toJSON() {
return '';
}
save() {}
translateHashToRison(stateHashOrRison: string | string[]) {
return stateHashOrRison;
}
getQueryParamName() {
return '';
}
}
return AppStateMock;

View file

@ -18,7 +18,7 @@
*/
/* global jest */
export function getEmbeddableFactoryMock(config) {
export function getEmbeddableFactoryMock(config?: any) {
const embeddableFactoryMockDefaults = {
create: jest.fn(() => Promise.resolve({})),
};

View file

@ -0,0 +1,45 @@
/*
* 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 { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
export function getSavedDashboardMock(
config?: Partial<SavedObjectDashboard>
): SavedObjectDashboard {
return {
id: '123',
title: 'my dashboard',
panelsJSON: '[]',
searchSource: {
getOwnField: (param: any) => param,
setField: () => {},
},
copyOnSave: false,
timeRestore: false,
timeTo: 'now',
timeFrom: 'now-15m',
optionsJSON: '',
lastSavedTitle: '',
destroy: () => {},
save: () => {
return Promise.resolve('123');
},
...config,
};
}

View file

@ -23,8 +23,9 @@ import _ from 'lodash';
import { createAction } from 'redux-actions';
import { EmbeddableMetadata, EmbeddableState } from 'ui/embeddable';
import { getEmbeddableCustomization, getPanel } from '../../selectors';
import { PanelId, PanelState } from '../selectors';
import { PanelId } from '../selectors';
import { updatePanel } from './panels';
import { SavedDashboardPanel } from '../types';
import { KibanaAction, KibanaThunk } from '../../selectors/types';
@ -113,7 +114,7 @@ export function embeddableStateChanged(changeData: {
const customization = getEmbeddableCustomization(getState(), panelId);
if (!_.isEqual(embeddableState.customization, customization)) {
const originalPanelState = getPanel(getState(), panelId);
const newPanelState: PanelState = {
const newPanelState: SavedDashboardPanel = {
...originalPanelState,
embeddableConfig: _.cloneDeep(embeddableState.customization),
};

View file

@ -39,7 +39,5 @@ export interface UpdateDescriptionAction
export type MetadataActions = UpdateDescriptionAction | UpdateTitleAction;
export const updateDescription = createAction<UpdateDescriptionAction>(
MetadataActionTypeKeys.UPDATE_DESCRIPTION
);
export const updateTitle = createAction<UpdateTitleAction>(MetadataActionTypeKeys.UPDATE_TITLE);
export const updateDescription = createAction<string>(MetadataActionTypeKeys.UPDATE_DESCRIPTION);
export const updateTitle = createAction<string>(MetadataActionTypeKeys.UPDATE_TITLE);

View file

@ -21,7 +21,8 @@
import { createAction } from 'redux-actions';
import { KibanaAction } from '../../selectors/types';
import { PanelId, PanelState, PanelStateMap } from '../selectors';
import { PanelId } from '../selectors';
import { SavedDashboardPanel, SavedDashboardPanelMap } from '../types';
export enum PanelActionTypeKeys {
DELETE_PANEL = 'DELETE_PANEL',
@ -36,10 +37,10 @@ export interface DeletePanelAction
extends KibanaAction<PanelActionTypeKeys.DELETE_PANEL, PanelId> {}
export interface UpdatePanelAction
extends KibanaAction<PanelActionTypeKeys.UPDATE_PANEL, PanelState> {}
extends KibanaAction<PanelActionTypeKeys.UPDATE_PANEL, SavedDashboardPanel> {}
export interface UpdatePanelsAction
extends KibanaAction<PanelActionTypeKeys.UPDATE_PANELS, PanelStateMap> {}
extends KibanaAction<PanelActionTypeKeys.UPDATE_PANELS, SavedDashboardPanelMap> {}
export interface ResetPanelTitleAction
extends KibanaAction<PanelActionTypeKeys.RESET_PANEL_TITLE, PanelId> {}
@ -53,7 +54,7 @@ export interface SetPanelTitleAction
extends KibanaAction<PanelActionTypeKeys.SET_PANEL_TITLE, SetPanelTitleActionPayload> {}
export interface SetPanelsAction
extends KibanaAction<PanelActionTypeKeys.SET_PANELS, PanelStateMap> {}
extends KibanaAction<PanelActionTypeKeys.SET_PANELS, SavedDashboardPanelMap> {}
export type PanelActions =
| DeletePanelAction
@ -64,10 +65,10 @@ export type PanelActions =
| SetPanelsAction;
export const deletePanel = createAction<PanelId>(PanelActionTypeKeys.DELETE_PANEL);
export const updatePanel = createAction<PanelState>(PanelActionTypeKeys.UPDATE_PANEL);
export const updatePanel = createAction<SavedDashboardPanel>(PanelActionTypeKeys.UPDATE_PANEL);
export const resetPanelTitle = createAction<PanelId>(PanelActionTypeKeys.RESET_PANEL_TITLE);
export const setPanelTitle = createAction<SetPanelTitleActionPayload>(
PanelActionTypeKeys.SET_PANEL_TITLE
);
export const updatePanels = createAction<PanelStateMap>(PanelActionTypeKeys.UPDATE_PANELS);
export const setPanels = createAction<PanelStateMap>(PanelActionTypeKeys.SET_PANELS);
export const updatePanels = createAction<SavedDashboardPanelMap>(PanelActionTypeKeys.UPDATE_PANELS);
export const setPanels = createAction<SavedDashboardPanelMap>(PanelActionTypeKeys.SET_PANELS);

View file

@ -108,4 +108,4 @@ export const updateRefreshConfig = createAction<RefreshConfig>(
ViewActionTypeKeys.UPDATE_REFRESH_CONFIG
);
export const updateFilters = createAction<Filters>(ViewActionTypeKeys.UPDATE_FILTERS);
export const updateQuery = createAction<Query>(ViewActionTypeKeys.UPDATE_QUERY);
export const updateQuery = createAction<Query | string>(ViewActionTypeKeys.UPDATE_QUERY);

View file

@ -110,7 +110,7 @@ app.directive('dashboardApp', function ($injector) {
const dashboardStateManager = new DashboardStateManager({
savedDashboard: dash,
AppState,
AppStateClass: AppState,
hideWriteControls: dashboardConfig.getHideWriteControls(),
addFilter: ({ field, value, operator, index }) => {
filterActions.addFilter(field, value, operator, index, dashboardStateManager.getAppState(), filterManager);

View file

@ -22,29 +22,45 @@ import { DashboardViewMode } from './dashboard_view_mode';
import { embeddableIsInitialized, setPanels } from './actions';
import { getAppStateMock, getSavedDashboardMock } from './__tests__';
import { store } from '../store';
import { AppStateClass } from 'ui/state_management/app_state';
import { DashboardAppState } from './types';
import { IndexPattern } from 'ui/index_patterns';
import { Timefilter } from 'ui/timefilter';
jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true });
describe('DashboardState', function () {
let dashboardState;
describe('DashboardState', function() {
let dashboardState: DashboardStateManager;
const savedDashboard = getSavedDashboardMock();
const mockTimefilter = {
time: {},
setTime: function (time) { this.time = time; },
const mockTimefilter: Timefilter = {
time: { to: 'now', from: 'now-15m' },
setTime(time) {
this.time = time;
},
getTime() {
return this.time;
},
disableAutoRefreshSelector: jest.fn(),
setRefreshInterval: jest.fn(),
getRefreshInterval: jest.fn(),
disableTimeRangeSelector: jest.fn(),
enableAutoRefreshSelector: jest.fn(),
off: jest.fn(),
on: jest.fn(),
};
const mockIndexPattern = { id: 'index1' };
const mockIndexPattern: IndexPattern = { id: 'index1', fields: [], title: 'hi' };
function initDashboardState() {
dashboardState = new DashboardStateManager({
savedDashboard,
AppState: getAppStateMock(),
AppStateClass: getAppStateMock() as AppStateClass<DashboardAppState>,
hideWriteControls: false,
addFilter: () => {},
});
}
describe('syncTimefilterWithDashboard', function () {
test('syncs quick time', function () {
describe('syncTimefilterWithDashboard', function() {
test('syncs quick time', function() {
savedDashboard.timeRestore = true;
savedDashboard.timeFrom = 'now/w';
savedDashboard.timeTo = 'now/w';
@ -59,7 +75,7 @@ describe('DashboardState', function () {
expect(mockTimefilter.time.from).toBe('now/w');
});
test('syncs relative time', function () {
test('syncs relative time', function() {
savedDashboard.timeRestore = true;
savedDashboard.timeFrom = 'now-13d';
savedDashboard.timeTo = 'now';
@ -74,7 +90,7 @@ describe('DashboardState', function () {
expect(mockTimefilter.time.from).toBe('now-13d');
});
test('syncs absolute time', function () {
test('syncs absolute time', function() {
savedDashboard.timeRestore = true;
savedDashboard.timeFrom = '2015-09-19 06:31:44.000';
savedDashboard.timeTo = '2015-09-29 06:31:44.000';
@ -90,7 +106,7 @@ describe('DashboardState', function () {
});
});
describe('isDirty', function () {
describe('isDirty', function() {
beforeAll(() => {
initDashboardState();
});
@ -108,15 +124,32 @@ describe('DashboardState', function () {
});
});
describe('panelIndexPatternMapping', function () {
describe('panelIndexPatternMapping', function() {
beforeAll(() => {
initDashboardState();
});
function simulateNewEmbeddableWithIndexPatterns({ panelId, indexPatterns }) {
store.dispatch(setPanels({ [panelId]: { panelIndex: panelId } }));
function simulateNewEmbeddableWithIndexPatterns({
panelId,
indexPatterns,
}: {
panelId: string;
indexPatterns?: IndexPattern[];
}) {
store.dispatch(
setPanels({
[panelId]: {
id: '123',
panelIndex: panelId,
version: '1',
type: 'hi',
embeddableConfig: {},
gridData: { x: 1, y: 1, h: 1, w: 1, i: '1' },
},
})
);
const metadata = { title: 'my embeddable title', editUrl: 'editme', indexPatterns };
store.dispatch(embeddableIsInitialized({ metadata, panelId: panelId }));
store.dispatch(embeddableIsInitialized({ metadata, panelId }));
}
test('initially has no index patterns', () => {
@ -124,13 +157,19 @@ describe('DashboardState', function () {
});
test('registers index pattern when an embeddable is initialized with one', async () => {
simulateNewEmbeddableWithIndexPatterns({ panelId: 'foo1', indexPatterns: [mockIndexPattern] });
simulateNewEmbeddableWithIndexPatterns({
panelId: 'foo1',
indexPatterns: [mockIndexPattern],
});
await new Promise(resolve => process.nextTick(resolve));
expect(dashboardState.getPanelIndexPatterns().length).toBe(1);
});
test('registers unique index patterns', async () => {
simulateNewEmbeddableWithIndexPatterns({ panelId: 'foo2', indexPatterns: [mockIndexPattern] });
simulateNewEmbeddableWithIndexPatterns({
panelId: 'foo2',
indexPatterns: [mockIndexPattern],
});
await new Promise(resolve => process.nextTick(resolve));
expect(dashboardState.getPanelIndexPatterns().length).toBe(1);
});

View file

@ -20,10 +20,18 @@
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory';
import { StaticIndexPattern } from 'ui/index_patterns';
import { AppStateClass as TAppStateClass } from 'ui/state_management/app_state';
import { TimeRange, Query } from 'ui/embeddable';
import { Timefilter } from 'ui/timefilter';
import { Filter } from '@kbn/es-query';
import moment from 'moment';
import { DashboardViewMode } from './dashboard_view_mode';
import { FilterUtils } from './lib/filter_utils';
import { PanelUtils } from './panel/panel_utils';
import { store } from '../store';
import {
updateViewMode,
setPanels,
@ -41,7 +49,6 @@ import {
closeContextMenu,
requestReload,
} from './actions';
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
import { createPanelState } from './panel';
import { getAppStateDefaults, migrateAppState } from './lib';
import {
@ -59,6 +66,16 @@ import {
getQuery,
getFilters,
} from '../selectors';
import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard';
import {
DashboardAppState,
SavedDashboardPanel,
SavedDashboardPanelMap,
StagedFilter,
DashboardAppStateParameters,
} from './types';
export type AddFilterFuntion = ({ field, value, operator, index }: StagedFilter) => void;
/**
* Dashboard state manager handles connecting angular and redux state between the angular and react portions of the
@ -67,21 +84,48 @@ import {
* versa. They should be as decoupled as possible so updating the store won't affect bwc of urls.
*/
export class DashboardStateManager {
public savedDashboard: SavedObjectDashboard;
public appState: DashboardAppState;
public lastSavedDashboardFilters: {
timeTo?: string | moment.Moment;
timeFrom?: string | moment.Moment;
filterBars: Filter[];
query: Query | string;
};
private stateDefaults: DashboardAppStateParameters;
private hideWriteControls: boolean;
public isDirty: boolean;
private changeListeners: Array<(status: { dirty: boolean }) => void>;
private stateMonitor: StateMonitor<DashboardAppStateParameters>;
private panelIndexPatternMapping: { [key: string]: StaticIndexPattern[] } = {};
private addFilter: AddFilterFuntion;
private unsubscribe: () => void;
/**
*
* @param {SavedDashboard} savedDashboard
* @param {AppState} AppState The AppState class to use when instantiating a new AppState instance.
* @param {boolean} hideWriteControls true if write controls should be hidden.
* @param {function} addFilter a function that can be used to add a filter bar filter
* @param savedDashboard
* @param AppState The AppState class to use when instantiating a new AppState instance.
* @param hideWriteControls true if write controls should be hidden.
* @param addFilter a function that can be used to add a filter bar filter
*/
constructor({ savedDashboard, AppState, hideWriteControls, addFilter }) {
constructor({
savedDashboard,
AppStateClass,
hideWriteControls,
addFilter,
}: {
savedDashboard: SavedObjectDashboard;
AppStateClass: TAppStateClass<DashboardAppState>;
hideWriteControls: boolean;
addFilter: AddFilterFuntion;
}) {
this.savedDashboard = savedDashboard;
this.hideWriteControls = hideWriteControls;
this.addFilter = addFilter;
this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls);
this.appState = new AppState(this.stateDefaults);
this.appState = new AppStateClass(this.stateDefaults);
// Initializing appState does two things - first it translates the defaults into AppState, second it updates
// appState based on the URL (the url trumps the defaults). This means if we update the state format at all and
@ -102,28 +146,44 @@ export class DashboardStateManager {
PanelUtils.initPanelIndexes(this.getPanels());
this.createStateMonitor();
/**
* Creates a state monitor and saves it to this.stateMonitor. Used to track unsaved changes made to appState.
*/
this.stateMonitor = stateMonitorFactory.create<DashboardAppStateParameters>(
this.appState,
this.stateDefaults
);
this.stateMonitor.ignoreProps('viewMode');
// Filters need to be compared manually because they sometimes have a $$hashkey stored on the object.
this.stateMonitor.ignoreProps('filters');
// Query needs to be compared manually because saved legacy queries get migrated in app state automatically
this.stateMonitor.ignoreProps('query');
this.stateMonitor.onChange((status: { dirty: boolean }) => {
this.isDirty = status.dirty;
});
store.dispatch(closeContextMenu());
// Always start out with all panels minimized when a dashboard is first loaded.
store.dispatch(minimizePanel());
this._pushAppStateChangesToStore();
this.pushAppStateChangesToStore();
this.changeListeners = [];
this.unsubscribe = store.subscribe(() => this._handleStoreChanges());
this.stateMonitor.onChange(status => {
this.unsubscribe = store.subscribe(() => this.handleStoreChanges());
this.stateMonitor.onChange((status: { dirty: boolean }) => {
this.changeListeners.forEach(listener => listener(status));
this._pushAppStateChangesToStore();
this.pushAppStateChangesToStore();
});
}
registerChangeListener(callback) {
public registerChangeListener(callback: (status: { dirty: boolean }) => void) {
this.changeListeners.push(callback);
}
_areStoreAndAppStatePanelsEqual() {
private areStoreAndAppStatePanelsEqual() {
const state = store.getState();
const storePanels = getPanels(store.getState());
const appStatePanels = this.getPanels();
@ -132,44 +192,46 @@ export class DashboardStateManager {
return false;
}
return appStatePanels.every((appStatePanel) => {
return appStatePanels.every(appStatePanel => {
const storePanel = getPanel(state, appStatePanel.panelIndex);
return _.isEqual(appStatePanel, storePanel);
});
}
/**
* Time is part of global state so we need to deal with it outside of _pushAppStateChangesToStore.
* @param {String|Object} newTimeFilter.to -- either a string representing an absolute time in utc format,
* or a relative time (now-15m), or a moment object
* @param {String|Object} newTimeFilter.from - either a string representing an absolute or a relative time, or a
* moment object
* Time is part of global state so we need to deal with it outside of pushAppStateChangesToStore.
*/
handleTimeChange(newTimeFilter) {
store.dispatch(updateTimeRange({
from: FilterUtils.convertTimeToUTCString(newTimeFilter.from),
to: FilterUtils.convertTimeToUTCString(newTimeFilter.to),
}));
public handleTimeChange(newTimeRange: TimeRange) {
const from = FilterUtils.convertTimeToUTCString(newTimeRange.from);
const to = FilterUtils.convertTimeToUTCString(newTimeRange.to);
store.dispatch(
updateTimeRange({
from: from ? from.toString() : '',
to: to ? to.toString() : '',
})
);
}
handleRefreshConfigChange({ pause, value }) {
store.dispatch(updateRefreshConfig({
isPaused: pause,
interval: value,
}));
public handleRefreshConfigChange({ pause, value }: { pause: boolean; value: number }) {
store.dispatch(
updateRefreshConfig({
isPaused: pause,
interval: value,
})
);
}
/**
* Changes made to app state outside of direct calls to this class will need to be propagated to the store.
* @private
*/
_pushAppStateChangesToStore() {
private pushAppStateChangesToStore() {
// We need these checks, or you can get into a loop where a change is triggered by the store, which updates
// AppState, which then dispatches the change here, which will end up triggering setState warnings.
if (!this._areStoreAndAppStatePanelsEqual()) {
if (!this.areStoreAndAppStatePanelsEqual()) {
// Translate appState panels data into the data expected by redux, copying the panel objects as we do so
// because the panels inside appState can be mutated, while redux state should never be mutated directly.
const panelsMap = this.getPanels().reduce((acc, panel) => {
const panelsMap = this.getPanels().reduce((acc: SavedDashboardPanelMap, panel) => {
acc[panel.panelIndex] = _.cloneDeep(panel);
return acc;
}, {});
@ -220,10 +282,12 @@ export class DashboardStateManager {
_pushFiltersToStore() {
const state = store.getState();
const dashboardFilters = this.getDashboardFilterBars();
if (!_.isEqual(
FilterUtils.cleanFiltersForComparison(dashboardFilters),
FilterUtils.cleanFiltersForComparison(getFilters(state))
)) {
if (
!_.isEqual(
FilterUtils.cleanFiltersForComparison(dashboardFilters),
FilterUtils.cleanFiltersForComparison(getFilters(state))
)
) {
store.dispatch(updateFilters(dashboardFilters));
}
}
@ -232,24 +296,28 @@ export class DashboardStateManager {
store.dispatch(requestReload());
}
_handleStoreChanges() {
private handleStoreChanges() {
let dirty = false;
if (!this._areStoreAndAppStatePanelsEqual()) {
const panels = getPanels(store.getState());
if (!this.areStoreAndAppStatePanelsEqual()) {
const panels: SavedDashboardPanelMap = getPanels(store.getState());
this.appState.panels = [];
this.panelIndexPatternMapping = {};
Object.values(panels).map(panel => {
Object.values(panels).map((panel: SavedDashboardPanel) => {
this.appState.panels.push(_.cloneDeep(panel));
});
dirty = true;
}
_.forEach(getEmbeddables(store.getState()), (embeddable, panelId) => {
if (embeddable.initialized && !this.panelIndexPatternMapping.hasOwnProperty(panelId)) {
if (
panelId &&
embeddable.initialized &&
!this.panelIndexPatternMapping.hasOwnProperty(panelId)
) {
const embeddableMetadata = getEmbeddableMetadata(store.getState(), panelId);
if (embeddableMetadata.indexPatterns) {
if (embeddableMetadata && embeddableMetadata.indexPatterns) {
this.panelIndexPatternMapping[panelId] = _.compact(embeddableMetadata.indexPatterns);
this.dirty = true;
dirty = true;
}
}
});
@ -272,16 +340,16 @@ export class DashboardStateManager {
this.saveState();
}
getFullScreenMode() {
public getFullScreenMode() {
return this.appState.fullScreenMode;
}
setFullScreenMode(fullScreenMode) {
public setFullScreenMode(fullScreenMode: boolean) {
this.appState.fullScreenMode = fullScreenMode;
this.saveState();
}
getPanelIndexPatterns() {
public getPanelIndexPatterns() {
const indexPatterns = _.flatten(Object.values(this.panelIndexPatternMapping));
return _.uniq(indexPatterns, 'id');
}
@ -289,7 +357,7 @@ export class DashboardStateManager {
/**
* Resets the state back to the last saved version of the dashboard.
*/
resetState() {
public resetState() {
// In order to show the correct warning, we have to store the unsaved
// title on the dashboard object. We should fix this at some point, but this is how all the other object
// save panels work at the moment.
@ -317,66 +385,68 @@ export class DashboardStateManager {
* Returns an object which contains the current filter state of this.savedDashboard.
* @returns {{timeTo: String, timeFrom: String, filterBars: Array, query: Object}}
*/
getFilterState() {
public getFilterState() {
return {
timeTo: this.savedDashboard.timeTo,
timeFrom: this.savedDashboard.timeFrom,
filterBars: this.getDashboardFilterBars(),
query: this.getDashboardQuery()
query: this.getDashboardQuery(),
};
}
getTitle() {
public getTitle() {
return this.appState.title;
}
getDescription() {
public getDescription() {
return this.appState.description;
}
setDescription(description) {
public setDescription(description: string) {
this.appState.description = description;
this.saveState();
}
setTitle(title) {
public setTitle(title: string) {
this.appState.title = title;
this.savedDashboard.title = title;
this.saveState();
}
getAppState() {
public getAppState() {
return this.appState;
}
getQuery() {
public getQuery() {
return this.appState.query;
}
getUseMargins() {
public getUseMargins() {
// Existing dashboards that don't define this should default to false.
return this.appState.options.useMargins === undefined ? false : this.appState.options.useMargins;
return this.appState.options.useMargins === undefined
? false
: this.appState.options.useMargins;
}
setUseMargins(useMargins) {
public setUseMargins(useMargins: boolean) {
this.appState.options.useMargins = useMargins;
this.saveState();
}
getHidePanelTitles() {
public getHidePanelTitles() {
return this.appState.options.hidePanelTitles;
}
setHidePanelTitles(hidePanelTitles) {
public setHidePanelTitles(hidePanelTitles: boolean) {
this.appState.options.hidePanelTitles = hidePanelTitles;
this.saveState();
}
getTimeRestore() {
public getTimeRestore() {
return this.appState.timeRestore;
}
setTimeRestore(timeRestore) {
public setTimeRestore(timeRestore: boolean) {
this.appState.timeRestore = timeRestore;
this.saveState();
}
@ -384,23 +454,23 @@ export class DashboardStateManager {
/**
* @returns {boolean}
*/
getIsTimeSavedWithDashboard() {
public getIsTimeSavedWithDashboard() {
return this.savedDashboard.timeRestore;
}
getDashboardFilterBars() {
public getDashboardFilterBars() {
return FilterUtils.getFilterBarsForDashboard(this.savedDashboard);
}
getDashboardQuery() {
public getDashboardQuery() {
return FilterUtils.getQueryFilterForDashboard(this.savedDashboard);
}
getLastSavedFilterBars() {
public getLastSavedFilterBars(): Filter[] {
return this.lastSavedDashboardFilters.filterBars;
}
getLastSavedQuery() {
public getLastSavedQuery(): Query | string {
return this.lastSavedDashboardFilters.query;
}
@ -408,17 +478,14 @@ export class DashboardStateManager {
* @returns {boolean} True if the query changed since the last time the dashboard was saved, or if it's a
* new dashboard, if the query differs from the default.
*/
getQueryChanged() {
public getQueryChanged() {
const currentQuery = this.appState.query;
const lastSavedQuery = this.getLastSavedQuery();
const isLegacyStringQuery = (
_.isString(lastSavedQuery)
&& _.isPlainObject(currentQuery)
&& _.has(currentQuery, 'query')
);
const isLegacyStringQuery =
_.isString(lastSavedQuery) && _.isPlainObject(currentQuery) && _.has(currentQuery, 'query');
if (isLegacyStringQuery) {
return lastSavedQuery !== currentQuery.query;
return (lastSavedQuery as string) !== (currentQuery as Query).query;
}
return !_.isEqual(currentQuery, lastSavedQuery);
@ -428,7 +495,7 @@ export class DashboardStateManager {
* @returns {boolean} True if the filter bar state has changed since the last time the dashboard was saved,
* or if it's a new dashboard, if the query differs from the default.
*/
getFilterBarChanged() {
public getFilterBarChanged() {
return !_.isEqual(
FilterUtils.cleanFiltersForComparison(this.appState.filters),
FilterUtils.cleanFiltersForComparison(this.getLastSavedFilterBars())
@ -439,9 +506,12 @@ export class DashboardStateManager {
* @param timeFilter
* @returns {boolean} True if the time state has changed since the time saved with the dashboard.
*/
getTimeChanged(timeFilter) {
public getTimeChanged(timeFilter: Timefilter) {
return (
!FilterUtils.areTimesEqual(this.lastSavedDashboardFilters.timeFrom, timeFilter.getTime().from) ||
!FilterUtils.areTimesEqual(
this.lastSavedDashboardFilters.timeFrom,
timeFilter.getTime().from
) ||
!FilterUtils.areTimesEqual(this.lastSavedDashboardFilters.timeTo, timeFilter.getTime().to)
);
}
@ -450,21 +520,21 @@ export class DashboardStateManager {
*
* @returns {DashboardViewMode}
*/
getViewMode() {
public getViewMode() {
return this.hideWriteControls ? DashboardViewMode.VIEW : this.appState.viewMode;
}
/**
* @returns {boolean}
*/
getIsViewMode() {
public getIsViewMode() {
return this.getViewMode() === DashboardViewMode.VIEW;
}
/**
* @returns {boolean}
*/
getIsEditMode() {
public getIsEditMode() {
return this.getViewMode() === DashboardViewMode.EDIT;
}
@ -472,22 +542,24 @@ export class DashboardStateManager {
*
* @returns {boolean} True if the dashboard has changed since the last save (or, is new).
*/
getIsDirty(timeFilter) {
public getIsDirty(timeFilter?: Timefilter) {
// Filter bar comparison is done manually (see cleanFiltersForComparison for the reason) and time picker
// changes are not tracked by the state monitor.
const hasTimeFilterChanged = timeFilter ? this.getFiltersChanged(timeFilter) : false;
return this.getIsEditMode() && (this.isDirty || hasTimeFilterChanged);
}
getPanels() {
public getPanels(): SavedDashboardPanel[] {
return this.appState.panels;
}
updatePanel(panelIndex, panelAttributes) {
const panel = this.getPanels().find((panel) => panel.panelIndex === panelIndex);
Object.assign(panel, panelAttributes);
public updatePanel(panelIndex: string, panelAttributes: any) {
const foundPanel = this.getPanels().find(
(panel: SavedDashboardPanel) => panel.panelIndex === panelIndex
);
Object.assign(foundPanel, panelAttributes);
this.saveState();
return panel;
return foundPanel;
}
/**
@ -495,15 +567,15 @@ export class DashboardStateManager {
* @param {number} id
* @param {string} type
*/
addNewPanel = (id, type) => {
public addNewPanel = (id: string, type: string) => {
const maxPanelIndex = PanelUtils.getMaxPanelIndex(this.getPanels());
const newPanel = createPanelState(id, type, maxPanelIndex, this.getPanels());
const newPanel = createPanelState(id, type, maxPanelIndex.toString(), this.getPanels());
this.getPanels().push(newPanel);
this.saveState();
}
};
removePanel(panelIndex) {
_.remove(this.getPanels(), (panel) => {
public removePanel(panelIndex: string) {
_.remove(this.getPanels(), panel => {
if (panel.panelIndex === panelIndex) {
delete this.panelIndexPatternMapping[panelIndex];
return true;
@ -518,7 +590,7 @@ export class DashboardStateManager {
* @param timeFilter
* @returns {Array.<string>} An array of user friendly strings indicating the filter types that have changed.
*/
getChangedFilterTypes(timeFilter) {
public getChangedFilterTypes(timeFilter: Timefilter) {
const changedFilters = [];
if (this.getFilterBarChanged()) {
changedFilters.push('filter');
@ -533,25 +605,24 @@ export class DashboardStateManager {
}
/**
* @return {boolean} True if filters (query, filter bar filters, and time picker if time is stored
* @return True if filters (query, filter bar filters, and time picker if time is stored
* with the dashboard) have changed since the last saved state (or if the dashboard hasn't been saved,
* the default state).
*/
getFiltersChanged(timeFilter) {
public getFiltersChanged(timeFilter: Timefilter) {
return this.getChangedFilterTypes(timeFilter).length > 0;
}
/**
* Updates timeFilter to match the time saved with the dashboard.
* @param {Object} timeFilter
* @param {func} timeFilter.setTime
* @param {func} timeFilter.setRefreshInterval
*/
syncTimefilterWithDashboard(timeFilter) {
public syncTimefilterWithDashboard(timeFilter: Timefilter) {
if (!this.getIsTimeSavedWithDashboard()) {
throw new Error(i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', {
defaultMessage: 'The time is not saved with this dashboard so should not be synced.',
}));
throw new Error(
i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', {
defaultMessage: 'The time is not saved with this dashboard so should not be synced.',
})
);
}
timeFilter.setTime({
@ -567,7 +638,7 @@ export class DashboardStateManager {
/**
* Saves the current application state to the URL.
*/
saveState() {
public saveState() {
this.appState.save();
}
@ -575,7 +646,7 @@ export class DashboardStateManager {
* Applies the current filter state to the dashboard.
* @param filter {Array.<Object>} An array of filter bar filters.
*/
applyFilters(query, filters) {
public applyFilters(query: Query, filters: Filter[]) {
this.appState.query = query;
this.savedDashboard.searchSource.setField('query', query);
this.savedDashboard.searchSource.setField('filter', filters);
@ -584,27 +655,10 @@ export class DashboardStateManager {
this._pushFiltersToStore();
}
/**
* Creates a state monitor and saves it to this.stateMonitor. Used to track unsaved changes made to appState.
*/
createStateMonitor() {
this.stateMonitor = stateMonitorFactory.create(this.appState, this.stateDefaults);
this.stateMonitor.ignoreProps('viewMode');
// Filters need to be compared manually because they sometimes have a $$hashkey stored on the object.
this.stateMonitor.ignoreProps('filters');
// Query needs to be compared manually because saved legacy queries get migrated in app state automatically
this.stateMonitor.ignoreProps('query');
this.stateMonitor.onChange(status => {
this.isDirty = status.dirty;
});
}
/**
* @param newMode {DashboardViewMode}
*/
switchViewMode(newMode) {
public switchViewMode(newMode: DashboardViewMode) {
this.appState.viewMode = newMode;
this.saveState();
}
@ -612,7 +666,7 @@ export class DashboardStateManager {
/**
* Destroys and cleans up this object when it's no longer used.
*/
destroy() {
public destroy() {
if (this.stateMonitor) {
this.stateMonitor.destroy();
}

View file

@ -37,8 +37,12 @@ import {
import { DashboardViewMode } from '../dashboard_view_mode';
import { DashboardPanel } from '../panel';
import { PanelUtils } from '../panel/panel_utils';
import { PanelState, PanelStateMap, Pre61PanelState } from '../selectors/types';
import { GridData } from '../types';
import {
GridData,
SavedDashboardPanel,
Pre61SavedDashboardPanel,
SavedDashboardPanelMap,
} from '../types';
let lastValidGridSize = 0;
@ -117,10 +121,10 @@ const config = { monitorWidth: true };
const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
interface Props extends ReactIntl.InjectedIntlProps {
panels: PanelStateMap;
panels: SavedDashboardPanelMap;
getEmbeddableFactory: (panelType: string) => EmbeddableFactory;
dashboardViewMode: DashboardViewMode.EDIT | DashboardViewMode.VIEW;
onPanelsUpdated: (updatedPanels: PanelStateMap) => void;
onPanelsUpdated: (updatedPanels: SavedDashboardPanelMap) => void;
maximizedPanelId?: string;
useMargins: boolean;
}
@ -182,18 +186,18 @@ class DashboardGridUi extends React.Component<Props, State> {
: PanelUtils.parseVersion('6.0.0');
if (panelVersion.major < 6 || (panelVersion.major === 6 && panelVersion.minor < 1)) {
panel = PanelUtils.convertPanelDataPre_6_1(panel as Pre61PanelState);
panel = PanelUtils.convertPanelDataPre_6_1((panel as unknown) as Pre61SavedDashboardPanel);
}
if (panelVersion.major < 6 || (panelVersion.major === 6 && panelVersion.minor < 3)) {
PanelUtils.convertPanelDataPre_6_3(panel as PanelState, this.props.useMargins);
PanelUtils.convertPanelDataPre_6_3(panel as SavedDashboardPanel, this.props.useMargins);
}
return (panel as PanelState).gridData;
return (panel as SavedDashboardPanel).gridData;
});
}
public createEmbeddableFactoriesMap(panels: PanelStateMap) {
public createEmbeddableFactoriesMap(panels: SavedDashboardPanelMap) {
Object.values(panels).map(panel => {
if (!this.embeddableFactoryMap[panel.type]) {
this.embeddableFactoryMap[panel.type] = this.props.getEmbeddableFactory(panel.type);
@ -211,17 +215,14 @@ class DashboardGridUi extends React.Component<Props, State> {
public onLayoutChange = (layout: PanelLayout[]) => {
const { onPanelsUpdated, panels } = this.props;
const updatedPanels = layout.reduce(
(updatedPanelsAcc, panelLayout) => {
updatedPanelsAcc[panelLayout.i] = {
...panels[panelLayout.i],
panelIndex: panelLayout.i,
gridData: _.pick(panelLayout, ['x', 'y', 'w', 'h', 'i']),
};
return updatedPanelsAcc;
},
{} as PanelStateMap
);
const updatedPanels = layout.reduce((updatedPanelsAcc: SavedDashboardPanelMap, panelLayout) => {
updatedPanelsAcc[panelLayout.i] = {
...panels[panelLayout.i],
panelIndex: panelLayout.i,
gridData: _.pick(panelLayout, ['x', 'y', 'w', 'h', 'i']),
};
return updatedPanelsAcc;
}, {});
onPanelsUpdated(updatedPanels);
};
@ -240,7 +241,9 @@ class DashboardGridUi extends React.Component<Props, State> {
const { focusedPanelIndex } = this.state;
// Part of our unofficial API - need to render in a consistent order for plugins.
const panelsInOrder = Object.keys(panels).map((key: string) => panels[key] as PanelState);
const panelsInOrder = Object.keys(panels).map(
(key: string) => panels[key] as SavedDashboardPanel
);
panelsInOrder.sort((panelA, panelB) => {
if (panelA.gridData.y === panelB.gridData.y) {
return panelA.gridData.x - panelB.gridData.x;

View file

@ -21,17 +21,18 @@ import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { updatePanels } from '../actions';
import { getPanels, getUseMargins, getViewMode } from '../selectors';
import { DashboardViewMode, PanelStateMap } from '../selectors/types';
import { DashboardViewMode } from '../selectors/types';
import { DashboardGrid } from './dashboard_grid';
import { SavedDashboardPanelMap } from '../types';
interface DashboardGridContainerStateProps {
panels: PanelStateMap;
panels: SavedDashboardPanelMap;
dashboardViewMode: DashboardViewMode;
useMargins: boolean;
}
interface DashboardGridContainerDispatchProps {
onPanelsUpdated(updatedPanels: PanelStateMap): void;
onPanelsUpdated(updatedPanels: SavedDashboardPanelMap): void;
}
const mapStateToProps = ({ dashboard }: any): any => ({
@ -41,7 +42,7 @@ const mapStateToProps = ({ dashboard }: any): any => ({
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
onPanelsUpdated: (updatedPanels: PanelStateMap) => dispatch(updatePanels(updatedPanels)),
onPanelsUpdated: (updatedPanels: SavedDashboardPanelMap) => dispatch(updatePanels(updatedPanels)),
});
export const DashboardGridContainer = connect<

View file

@ -18,7 +18,10 @@
*/
import _ from 'lodash';
import moment from 'moment';
import moment, { Moment } from 'moment';
import { QueryFilter } from 'ui/filter_manager/query_filter';
import { Filter } from '@kbn/es-query';
import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
/**
* @typedef {Object} QueryFilter
@ -33,7 +36,7 @@ export class FilterUtils {
* @returns {Boolean} True if the filter is of the special query type
* (e.g. goes in the query input bar), false otherwise (e.g. is in the filter bar).
*/
static isQueryFilter(filter) {
public static isQueryFilter(filter: Filter) {
return filter.query && !filter.meta;
}
@ -43,7 +46,7 @@ export class FilterUtils {
* @returns {Array.<Object>} An array of filters stored with the dashboard. Includes
* both query filters and filter bar filters.
*/
static getDashboardFilters(dashboard) {
public static getDashboardFilters(dashboard: SavedObjectDashboard): Filter[] {
return dashboard.searchSource.getOwnField('filter');
}
@ -52,7 +55,7 @@ export class FilterUtils {
* @param {SavedDashboard} dashboard
* @returns {QueryFilter}
*/
static getQueryFilterForDashboard(dashboard) {
public static getQueryFilterForDashboard(dashboard: SavedObjectDashboard): QueryFilter | string {
if (dashboard.searchSource.getOwnField('query')) {
return dashboard.searchSource.getOwnField('query');
}
@ -68,21 +71,23 @@ export class FilterUtils {
* @return {Array.<Object>} Array of filters that should appear in the filter bar for the
* given dashboard
*/
static getFilterBarsForDashboard(dashboard) {
public static getFilterBarsForDashboard(dashboard: SavedObjectDashboard) {
return _.reject(this.getDashboardFilters(dashboard), this.isQueryFilter);
}
/**
* Converts the time to a utc formatted string. If the time is not valid (e.g. it might be in a relative format like
* 'now-15m', then it just returns what it was passed).
* Note** Changing these moment objects to a utc string will actually cause a bug because it'll be in a format not
* expected by the time picker. This should get cleaned up and we should pick a single format to use everywhere.
* @param time {string|Moment}
* @returns {string} the time represented in utc format, or if the time range was not able to be parsed into a moment
* object, it returns the same object it was given.
*/
static convertTimeToUTCString(time) {
public static convertTimeToUTCString(time?: string | Moment): undefined | string | moment.Moment {
if (moment(time).isValid()) {
return moment(time).utc();
} else {
} else {
return time;
}
}
@ -95,7 +100,7 @@ export class FilterUtils {
* @param timeB {string|Moment}
* @returns {boolean}
*/
static areTimesEqual(timeA, timeB) {
public static areTimesEqual(timeA?: string | Moment, timeB?: string | Moment) {
return this.convertTimeToUTCString(timeA) === this.convertTimeToUTCString(timeB);
}
@ -105,7 +110,7 @@ export class FilterUtils {
* @param filters {Array.<Object>}
* @returns {Array.<Object>}
*/
static cleanFiltersForComparison(filters) {
return _.map(filters, (filter) => _.omit(filter, ['$$hashKey', '$state']));
public static cleanFiltersForComparison(filters: Filter[]) {
return _.map(filters, filter => _.omit(filter, ['$$hashKey', '$state']));
}
}

View file

@ -19,25 +19,35 @@
import { DashboardViewMode } from '../dashboard_view_mode';
import { FilterUtils } from './filter_utils';
import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
import {
Pre61SavedDashboardPanel,
Pre64SavedDashboardPanel,
DashboardAppStateParameters,
} from '../types';
export function getAppStateDefaults(savedDashboard, hideWriteControls) {
export function getAppStateDefaults(
savedDashboard: SavedObjectDashboard,
hideWriteControls: boolean
): DashboardAppStateParameters {
const appState = {
fullScreenMode: false,
title: savedDashboard.title,
description: savedDashboard.description,
description: savedDashboard.description || '',
timeRestore: savedDashboard.timeRestore,
panels: savedDashboard.panelsJSON ? JSON.parse(savedDashboard.panelsJSON) : [],
options: savedDashboard.optionsJSON ? JSON.parse(savedDashboard.optionsJSON) : {},
query: FilterUtils.getQueryFilterForDashboard(savedDashboard),
filters: FilterUtils.getFilterBarsForDashboard(savedDashboard),
viewMode: savedDashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT,
viewMode:
savedDashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT,
};
// For BWC in pre 6.1 versions where uiState was stored at the dashboard level, not at the panel level.
// TODO: introduce a migration for this
if (savedDashboard.uiStateJSON) {
const uiState = JSON.parse(savedDashboard.uiStateJSON);
appState.panels.forEach(panel => {
appState.panels.forEach((panel: Pre61SavedDashboardPanel) => {
panel.embeddableConfig = uiState[`P-${panel.panelIndex}`];
});
delete savedDashboard.uiStateJSON;
@ -45,18 +55,17 @@ export function getAppStateDefaults(savedDashboard, hideWriteControls) {
// For BWC of pre 6.4 where search embeddables stored state directly on the panel and not under embeddableConfig.
// TODO: introduce a migration for this
appState.panels.forEach(panel => {
appState.panels.forEach((panel: Pre64SavedDashboardPanel) => {
if (panel.columns || panel.sort) {
panel.embeddableConfig = {
...panel.embeddableConfig,
columns: panel.columns,
sort: panel.sort
sort: panel.sort,
};
delete panel.columns;
delete panel.sort;
}
});
return appState;
}

View file

@ -17,15 +17,17 @@
* under the License.
*/
import { SavedDashboardPanel, DashboardAppState } from '../types';
/**
* Creates a new instance of AppState based of the saved dashboard.
*
* @param appState {AppState} AppState class to instantiate
*/
export function migrateAppState(appState) {
export function migrateAppState(appState: DashboardAppState) {
// For BWC in pre 6.1 versions where uiState was stored at the dashboard level, not at the panel level.
if (appState.uiState) {
appState.panels.forEach(panel => {
appState.panels.forEach((panel: SavedDashboardPanel) => {
panel.embeddableConfig = appState.uiState[`P-${panel.panelIndex}`];
});
delete appState.uiState;

View file

@ -17,25 +17,25 @@
* under the License.
*/
import { SaveOptions } from 'ui/saved_objects/saved_object';
import { Timefilter } from 'ui/timefilter';
import { updateSavedDashboard } from './update_saved_dashboard';
import { DashboardStateManager } from '../dashboard_state_manager';
/**
* Saves the dashboard.
* @param toJson {function} A custom toJson function. Used because the previous code used
* @param toJson A custom toJson function. Used because the previous code used
* the angularized toJson version, and it was unclear whether there was a reason not to use
* JSON.stringify
* @param timeFilter
* @param dashboardStateManager {DashboardStateManager}
* @param {object} [saveOptions={}]
* @property {boolean} [saveOptions.confirmOverwrite=false] - If true, attempts to create the source so it
* can confirm an overwrite if a document with the id already exists.
* @property {boolean} [saveOptions.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title
* @property {func} [saveOptions.onTitleDuplicate] - function called if duplicate title exists.
* When not provided, confirm modal will be displayed asking user to confirm or cancel save.
* @returns {Promise<string>} A promise that if resolved, will contain the id of the newly saved
* @returns A promise that if resolved, will contain the id of the newly saved
* dashboard.
*/
export function saveDashboard(toJson, timeFilter, dashboardStateManager, saveOptions) {
export function saveDashboard(
toJson: (obj: any) => string,
timeFilter: Timefilter,
dashboardStateManager: DashboardStateManager,
saveOptions: SaveOptions
): Promise<string> {
dashboardStateManager.saveState();
const savedDashboard = dashboardStateManager.savedDashboard;
@ -43,10 +43,9 @@ export function saveDashboard(toJson, timeFilter, dashboardStateManager, saveOpt
updateSavedDashboard(savedDashboard, appState, timeFilter, toJson);
return savedDashboard.save(saveOptions)
.then((id) => {
dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState();
dashboardStateManager.resetState();
return id;
});
return savedDashboard.save(saveOptions).then((id: string) => {
dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState();
dashboardStateManager.resetState();
return id;
});
}

View file

@ -18,21 +18,35 @@
*/
import _ from 'lodash';
import { AppState } from 'ui/state_management/app_state';
import { Timefilter } from 'ui/timefilter';
import { RefreshInterval } from 'ui/timefilter/timefilter';
import { FilterUtils } from './filter_utils';
import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
export function updateSavedDashboard(savedDashboard, appState, timeFilter, toJson) {
export function updateSavedDashboard(
savedDashboard: SavedObjectDashboard,
appState: AppState,
timeFilter: Timefilter,
toJson: <T>(object: T) => string
) {
savedDashboard.title = appState.title;
savedDashboard.description = appState.description;
savedDashboard.timeRestore = appState.timeRestore;
savedDashboard.panelsJSON = toJson(appState.panels);
savedDashboard.optionsJSON = toJson(appState.options);
savedDashboard.timeFrom = savedDashboard.timeRestore ?
FilterUtils.convertTimeToUTCString(timeFilter.getTime().from)
savedDashboard.timeFrom = savedDashboard.timeRestore
? FilterUtils.convertTimeToUTCString(timeFilter.getTime().from)
: undefined;
savedDashboard.timeTo = savedDashboard.timeRestore ?
FilterUtils.convertTimeToUTCString(timeFilter.getTime().to)
savedDashboard.timeTo = savedDashboard.timeRestore
? FilterUtils.convertTimeToUTCString(timeFilter.getTime().to)
: undefined;
const timeRestoreObj = _.pick(timeFilter.getRefreshInterval(), ['display', 'pause', 'section', 'value']);
const timeRestoreObj: RefreshInterval = _.pick(timeFilter.getRefreshInterval(), [
'display',
'pause',
'section',
'value',
]);
savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined;
}

View file

@ -18,10 +18,15 @@
*/
import expect from '@kbn/expect';
import { PanelState } from '../../selectors';
import { createPanelState } from '../panel_state';
import { SavedDashboardPanel } from '../../types';
function createPanelWithDimensions(x: number, y: number, w: number, h: number): PanelState {
function createPanelWithDimensions(
x: number,
y: number,
w: number,
h: number
): SavedDashboardPanel {
return {
id: 'foo',
version: '6.3.0',

View file

@ -30,9 +30,10 @@ import {
EmbeddableState,
} from 'ui/embeddable';
import { EmbeddableErrorAction } from '../actions';
import { PanelId, PanelState } from '../selectors';
import { PanelId } from '../selectors';
import { PanelError } from './panel_error';
import { PanelHeader } from './panel_header';
import { SavedDashboardPanel } from '../types';
export interface DashboardPanelProps {
viewOnlyMode: boolean;
@ -48,7 +49,7 @@ export interface DashboardPanelProps {
embeddableError: (errorMessage: EmbeddableErrorAction) => void;
embeddableIsInitializing: () => void;
initialized: boolean;
panel: PanelState;
panel: SavedDashboardPanel;
className?: string;
}

View file

@ -47,9 +47,9 @@ import {
getPanelType,
getViewMode,
PanelId,
PanelState,
} from '../selectors';
import { DashboardPanel } from './dashboard_panel';
import { SavedDashboardPanel } from '../types';
export interface DashboardPanelContainerOwnProps {
panelId: PanelId;
@ -61,7 +61,7 @@ interface DashboardPanelContainerStateProps {
viewOnlyMode: boolean;
containerState: ContainerState;
initialized: boolean;
panel: PanelState;
panel: SavedDashboardPanel;
lastReloadRequestTime?: number;
}

View file

@ -0,0 +1,72 @@
/*
* 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.
*/
jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true });
jest.mock('ui/metadata', () => ({
metadata: {
branch: 'my-metadata-branch',
version: 'my-metadata-version',
},
}));
import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants';
import { SavedDashboardPanel } from '../types';
import { createPanelState } from './panel_state';
const panels: SavedDashboardPanel[] = [];
test('createPanelState adds a new panel state in 0,0 position', () => {
const panelState = createPanelState('id', 'type', '1', panels);
expect(panelState.type).toBe('type');
expect(panelState.gridData.x).toBe(0);
expect(panelState.gridData.y).toBe(0);
expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
panels.push(panelState);
});
test('createPanelState adds a second new panel state', () => {
const panelState = createPanelState('id2', 'type', '2', panels);
expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH);
expect(panelState.gridData.y).toBe(0);
expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
panels.push(panelState);
});
test('createPanelState adds a third new panel state', () => {
const panelState = createPanelState('id3', 'type', '3', panels);
expect(panelState.gridData.x).toBe(0);
expect(panelState.gridData.y).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
panels.push(panelState);
});
test('createPanelState adds a new panel state in the top most position', () => {
const panelsWithEmptySpace = panels.filter(panel => panel.gridData.x === 0);
const panelState = createPanelState('id3', 'type', '3', panelsWithEmptySpace);
expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH);
expect(panelState.gridData.y).toBe(0);
expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
});

View file

@ -23,29 +23,14 @@ import {
DEFAULT_PANEL_HEIGHT,
DEFAULT_PANEL_WIDTH,
} from '../dashboard_constants';
import { PanelState } from '../selectors';
/**
* Represents a panel on a grid. Keeps track of position in the grid and what visualization it
* contains.
*
* @typedef {Object} PanelState
* @property {number} id - Id of the visualization contained in the panel.
* @property {string} version - Version of Kibana this panel was created in.
* @property {string} type - Type of the visualization in the panel.
* @property {number} panelIndex - Unique id to represent this panel in the grid. Note that this is
* NOT the index in the panels array. While it may initially represent that, it is not
* updated with changes in a dashboard, and is simply used as a unique identifier. The name
* remains as panelIndex for backward compatibility reasons - changing it can break reporting.
* @property {Object} gridData
* @property {number} gridData.w - Width of the panel.
* @property {number} gridData.h - Height of the panel.
* @property {number} gridData.x - Column position of the panel.
* @property {number} gridData.y - Row position of the panel.
*/
import { SavedDashboardPanel } from '../types';
// Look for the smallest y and x value where the default panel will fit.
function findTopLeftMostOpenSpace(width: number, height: number, currentPanels: PanelState[]) {
function findTopLeftMostOpenSpace(
width: number,
height: number,
currentPanels: SavedDashboardPanel[]
) {
let maxY = -1;
currentPanels.forEach(panel => {
@ -65,6 +50,14 @@ function findTopLeftMostOpenSpace(width: number, height: number, currentPanels:
currentPanels.forEach(panel => {
for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) {
for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) {
const row = grid[y];
if (row === undefined) {
throw new Error(
`Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify(
panel
)}`
);
}
grid[y][x] = 1;
}
}
@ -96,22 +89,17 @@ function findTopLeftMostOpenSpace(width: number, height: number, currentPanels:
}
}
}
return { x: 0, y: Infinity };
return { x: 0, y: maxY };
}
/**
* Creates and initializes a basic panel state.
* @param {number} id
* @param {string} type
* @param {number} panelIndex
* @param {Array} currentPanels
* @return {PanelState}
*/
export function createPanelState(
id: string,
type: string,
panelIndex: string,
currentPanels: PanelState[]
currentPanels: SavedDashboardPanel[]
) {
const { x, y } = findTopLeftMostOpenSpace(
DEFAULT_PANEL_WIDTH,

View file

@ -21,8 +21,7 @@ import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import chrome from 'ui/chrome';
import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants';
import { PanelState } from '../selectors';
import { GridData } from '../types';
import { GridData, SavedDashboardPanel } from '../types';
const PANEL_HEIGHT_SCALE_FACTOR = 5;
const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4;
@ -36,7 +35,7 @@ export interface SemanticVersion {
export class PanelUtils {
// 6.1 switched from gridster to react grid. React grid uses different variables for tracking layout
// eslint-disable-next-line @typescript-eslint/camelcase
public static convertPanelDataPre_6_1(panel: any): PanelState {
public static convertPanelDataPre_6_1(panel: any): SavedDashboardPanel {
['col', 'row'].forEach(key => {
if (!_.has(panel, key)) {
throw new Error(
@ -126,7 +125,7 @@ export class PanelUtils {
};
}
public static initPanelIndexes(panels: PanelState[]): void {
public static initPanelIndexes(panels: SavedDashboardPanel[]): void {
// find the largest panelIndex in all the panels
let maxIndex = this.getMaxPanelIndex(panels);
@ -138,7 +137,7 @@ export class PanelUtils {
});
}
public static getMaxPanelIndex(panels: PanelState[]): number {
public static getMaxPanelIndex(panels: SavedDashboardPanel[]): number {
let maxId = panels.reduce((id, panel) => {
return Math.max(id, Number(panel.panelIndex || id));
}, 0);

View file

@ -20,7 +20,12 @@
import _ from 'lodash';
import { Reducer } from 'redux';
import { PanelActions, PanelActionTypeKeys, SetPanelTitleActionPayload } from '../actions';
import { PanelId, PanelState, PanelStateMap } from '../selectors';
import { PanelId } from '../selectors';
import { SavedDashboardPanel } from '../types';
interface PanelStateMap {
[key: string]: SavedDashboardPanel;
}
const deletePanel = (panels: PanelStateMap, panelId: PanelId): PanelStateMap => {
const panelsCopy = { ...panels };
@ -28,7 +33,7 @@ const deletePanel = (panels: PanelStateMap, panelId: PanelId): PanelStateMap =>
return panelsCopy;
};
const updatePanel = (panels: PanelStateMap, panelState: PanelState): PanelStateMap => ({
const updatePanel = (panels: PanelStateMap, panelState: SavedDashboardPanel): PanelStateMap => ({
...panels,
[panelState.panelIndex]: panelState,
});

View file

@ -0,0 +1,43 @@
/*
* 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';
import { SavedObject } from 'ui/saved_objects/saved_object';
import moment from 'moment';
import { RefreshInterval } from 'ui/timefilter/timefilter';
export interface SavedObjectDashboard extends SavedObject {
id?: string;
copyOnSave: boolean;
timeRestore: boolean;
// These optionally being moment objects rather than strings seems more like a bug than by design. It's due to
// some code in udpate_saved_dashboard that should probably get cleaned up.
timeTo: string | moment.Moment | undefined;
timeFrom: string | moment.Moment | undefined;
title: string;
description?: string;
panelsJSON: string;
optionsJSON: string | undefined;
// TODO: write a migration to rid of this, it's only around for bwc.
uiStateJSON?: string;
lastSavedTitle: string;
searchSource: SearchSource;
destroy: () => void;
refreshInterval?: RefreshInterval;
}

View file

@ -18,15 +18,9 @@
*/
import _ from 'lodash';
import {
ContainerState,
EmbeddableMetadata,
Filters,
Query,
RefreshConfig,
TimeRange,
} from 'ui/embeddable';
import { ContainerState, EmbeddableMetadata, Query, RefreshConfig, TimeRange } from 'ui/embeddable';
import { EmbeddableCustomization } from 'ui/embeddable/types';
import { Filter } from '@kbn/es-query';
import { DashboardViewMode } from '../dashboard_view_mode';
import {
DashboardMetadata,
@ -34,14 +28,14 @@ import {
EmbeddableReduxState,
EmbeddablesMap,
PanelId,
PanelState,
PanelStateMap,
} from './types';
import { SavedDashboardPanel, SavedDashboardPanelMap, StagedFilter } from '../types';
export const getPanels = (dashboard: DashboardState): Readonly<PanelStateMap> => dashboard.panels;
export const getPanels = (dashboard: DashboardState): Readonly<SavedDashboardPanelMap> =>
dashboard.panels;
export const getPanel = (dashboard: DashboardState, panelId: PanelId): PanelState =>
getPanels(dashboard)[panelId] as PanelState;
export const getPanel = (dashboard: DashboardState, panelId: PanelId): SavedDashboardPanel =>
getPanels(dashboard)[panelId] as SavedDashboardPanel;
export const getPanelType = (dashboard: DashboardState, panelId: PanelId): string =>
getPanel(dashboard, panelId).type;
@ -118,7 +112,7 @@ export const getTimeRange = (dashboard: DashboardState): TimeRange => dashboard.
export const getRefreshConfig = (dashboard: DashboardState): RefreshConfig =>
dashboard.view.refreshConfig;
export const getFilters = (dashboard: DashboardState): Filters => dashboard.view.filters;
export const getFilters = (dashboard: DashboardState): Filter[] => dashboard.view.filters;
export const getQuery = (dashboard: DashboardState): Query => dashboard.view.query;
@ -150,5 +144,5 @@ export const getContainerState = (dashboard: DashboardState, panelId: PanelId):
/**
* @return an array of filters any embeddables wish dashboard to apply
*/
export const getStagedFilters = (dashboard: DashboardState): Filters =>
export const getStagedFilters = (dashboard: DashboardState): StagedFilter[] =>
_.compact(_.map(dashboard.embeddables, 'stagedFilter'));

View file

@ -17,9 +17,10 @@
* under the License.
*/
import { EmbeddableMetadata, Filters, Query, RefreshConfig, TimeRange } from 'ui/embeddable';
import { EmbeddableMetadata, Query, RefreshConfig, TimeRange } from 'ui/embeddable';
import { Filter } from '@kbn/es-query';
import { DashboardViewMode } from '../dashboard_view_mode';
import { GridData } from '../types';
import { SavedDashboardPanelMap } from '../types';
export type DashboardViewMode = DashboardViewMode;
export interface ViewState {
@ -32,22 +33,12 @@ export interface ViewState {
readonly hidePanelTitles: boolean;
readonly useMargins: boolean;
readonly query: Query;
readonly filters: Filters;
readonly filters: Filter[];
}
export type PanelId = string;
export type SavedObjectId = string;
export interface PanelState {
readonly id: SavedObjectId;
readonly version: string;
readonly type: string;
panelIndex: PanelId;
readonly embeddableConfig: any;
readonly gridData: GridData;
readonly title?: string;
}
export interface EmbeddableReduxState {
readonly metadata?: EmbeddableMetadata;
readonly error?: string | object;
@ -59,23 +50,6 @@ export interface EmbeddableReduxState {
readonly lastReloadRequestTime: number;
}
export interface Pre61PanelState {
size_x: number;
size_y: number;
row: number;
col: number;
panelIndex: any; // earlier versions allowed this to be number or string
id: string;
type: string;
// Embeddableconfig didn't actually exist on older panel states but `migrate_app_state.js` handles
// stuffing it on.
embeddableConfig: any;
}
export interface PanelStateMap {
[panelId: string]: PanelState | Pre61PanelState;
}
export interface EmbeddablesMap {
readonly [panelId: string]: EmbeddableReduxState;
}
@ -87,7 +61,7 @@ export interface DashboardMetadata {
export interface DashboardState {
readonly view: ViewState;
readonly panels: PanelStateMap;
readonly panels: SavedDashboardPanelMap;
readonly embeddables: EmbeddablesMap;
readonly metadata: DashboardMetadata;
}

View file

@ -17,6 +17,11 @@
* under the License.
*/
import { Query } from 'ui/embeddable';
import { AppState } from 'ui/state_management/app_state';
import { Filter } from '@kbn/es-query';
import { DashboardViewMode } from './dashboard_view_mode';
export interface GridData {
w: number;
h: number;
@ -24,3 +29,84 @@ export interface GridData {
y: number;
i: string;
}
export interface SavedDashboardPanel {
// TODO: Make id optional when embeddable API V2 is merged. At that point, it's okay to store panels
// that aren't backed by saved object ids.
readonly id: string;
readonly version: string;
readonly type: string;
panelIndex: string;
embeddableConfig: any;
readonly gridData: GridData;
readonly title?: string;
}
export interface Pre61SavedDashboardPanel {
readonly size_x: number;
readonly size_y: number;
readonly row: number;
readonly col: number;
readonly panelIndex: number | string; // earlier versions allowed this to be number or string
readonly id: string;
readonly type: string;
embeddableConfig: any;
}
export interface Pre64SavedDashboardPanel {
columns?: string;
sort?: string;
readonly id?: string;
readonly version: string;
readonly type: string;
readonly panelIndex: string;
readonly gridData: GridData;
readonly title?: string;
embeddableConfig: any;
}
export interface DashboardAppStateDefaults {
panels: SavedDashboardPanel[];
fullScreenMode: boolean;
title: string;
description?: string;
timeRestore: boolean;
options: {
useMargins: boolean;
hidePanelTitles: boolean;
};
query: Query;
filters: Filter[];
viewMode: DashboardViewMode;
}
export interface DashboardAppStateParameters {
panels: SavedDashboardPanel[];
fullScreenMode: boolean;
title: string;
description: string;
timeRestore: boolean;
options: {
hidePanelTitles: boolean;
useMargins: boolean;
};
query: Query | string;
filters: Filter[];
viewMode: DashboardViewMode;
}
// This could probably be improved if we flesh out AppState more... though AppState will be going away
// so maybe not worth too much time atm.
export type DashboardAppState = DashboardAppStateParameters & AppState;
export interface SavedDashboardPanelMap {
[key: string]: SavedDashboardPanel;
}
export interface StagedFilter {
field: string;
value: string;
operator: string;
index: string;
}

View file

@ -17,11 +17,13 @@
* under the License.
*/
import { Filters, Query, TimeRange } from 'ui/embeddable';
import { Query, TimeRange } from 'ui/embeddable';
import { Filter } from '@kbn/es-query';
import { DashboardViewMode } from '../dashboard/dashboard_view_mode';
import * as DashboardSelectors from '../dashboard/selectors';
import { PanelId } from '../dashboard/selectors/types';
import { CoreKibanaState } from './types';
import { StagedFilter } from '../dashboard/types';
export const getDashboard = (state: CoreKibanaState): DashboardSelectors.DashboardState =>
state.dashboard;
@ -46,7 +48,7 @@ export const getEmbeddableStagedFilter = (state: CoreKibanaState, panelId: Panel
export const getEmbeddableMetadata = (state: CoreKibanaState, panelId: PanelId) =>
DashboardSelectors.getEmbeddableMetadata(getDashboard(state), panelId);
export const getStagedFilters = (state: CoreKibanaState): Filters =>
export const getStagedFilters = (state: CoreKibanaState): StagedFilter[] =>
DashboardSelectors.getStagedFilters(getDashboard(state));
export const getViewMode = (state: CoreKibanaState): DashboardViewMode =>
DashboardSelectors.getViewMode(getDashboard(state));
@ -60,7 +62,7 @@ export const getHidePanelTitles = (state: CoreKibanaState): boolean =>
DashboardSelectors.getHidePanelTitles(getDashboard(state));
export const getTimeRange = (state: CoreKibanaState): TimeRange =>
DashboardSelectors.getTimeRange(getDashboard(state));
export const getFilters = (state: CoreKibanaState): Filters =>
export const getFilters = (state: CoreKibanaState): Filter[] =>
DashboardSelectors.getFilters(getDashboard(state));
export const getQuery = (state: CoreKibanaState): Query =>
DashboardSelectors.getQuery(getDashboard(state));

View file

@ -18,6 +18,7 @@
*/
import { Adapters } from 'ui/inspector';
import { StaticIndexPattern } from 'ui/index_patterns';
import { ContainerState } from './types';
export interface EmbeddableMetadata {
@ -25,7 +26,7 @@ export interface EmbeddableMetadata {
* Should specify any index pattern the embeddable uses. This will be used by the container to list out
* available fields to filter on.
*/
indexPatterns?: object[];
indexPatterns?: StaticIndexPattern[];
/**
* The title, or name, of the embeddable.

View file

@ -21,4 +21,12 @@ export { EmbeddableFactory, OnEmbeddableStateChanged } from './embeddable_factor
export * from './embeddable';
export * from './context_menu_actions';
export { EmbeddableFactoriesRegistryProvider } from './embeddable_factories_registry';
export { ContainerState, EmbeddableState, Query, Filters, TimeRange, RefreshConfig } from './types';
export {
ContainerState,
EmbeddableState,
Query,
Filters,
Filter,
TimeRange,
RefreshConfig,
} from './types';

View file

@ -17,6 +17,11 @@
* under the License.
*/
import { Filter } from '@kbn/es-query';
// Should go away soon once everyone imports from kbn/es-query
export { Filter } from '@kbn/es-query';
export interface TimeRange {
to: string;
from: string;
@ -31,12 +36,6 @@ export interface FilterMeta {
disabled: boolean;
}
// TODO: Filter object representation needs to be fleshed out.
export interface Filter {
meta: FilterMeta;
query: object;
}
export type Filters = Filter[];
export enum QueryLanguageType {
@ -44,10 +43,13 @@ export enum QueryLanguageType {
LUCENE = 'lucene',
}
// It's a string sometimes in old version formats, before Kuery came along and there
// was the language specifier.
export interface Query {
language: QueryLanguageType;
query: string;
}
export interface EmbeddableCustomization {
[key: string]: object | string;
}
@ -58,7 +60,7 @@ export interface ContainerState {
timeRange: TimeRange;
filters: Filters;
filters: Filter[];
refreshConfig: RefreshConfig;

View file

@ -17,15 +17,13 @@
* under the License.
*/
export function getSavedDashboardMock(config) {
const defaults = {
id: '123',
title: 'my dashboard',
panelsJSON: '[]',
searchSource: {
getOwnField: (param) => param
}
};
return Object.assign(defaults, config);
export interface SaveOptions {
confirmOverwrite: boolean;
isTitleDuplicateConfirmed: boolean;
onTitleDuplicate: () => void;
}
export interface SavedObject {
save: (saveOptions: SaveOptions) => Promise<string>;
copyOnSave: boolean;
}

View file

@ -18,4 +18,9 @@
*/
import { State } from './state';
export type AppState = State;
export class AppState extends State {}
export type AppStateClass<
TAppState extends AppState = AppState,
TDefaults = { [key: string]: any }
> = new (defaults: TDefaults) => TAppState;

View file

@ -17,7 +17,7 @@
* under the License.
*/
export interface State {
export class State {
[key: string]: any;
translateHashToRison: (stateHashOrRison: string | string[]) => string | string[];
getQueryParamName: () => string;

View file

@ -20,7 +20,10 @@ import { cloneDeep, isEqual, isPlainObject, set } from 'lodash';
import { State } from './state';
export const stateMonitorFactory = {
create: (state: State, customInitialState: State) => stateMonitor(state, customInitialState),
create: <TStateDefault extends { [key: string]: unknown }>(
state: State,
customInitialState: TStateDefault
) => stateMonitor<TStateDefault>(state, customInitialState),
};
interface StateStatus {
@ -28,22 +31,32 @@ interface StateStatus {
dirty: boolean;
}
export interface StateMonitor<TStateDefault extends { [key: string]: unknown }> {
setInitialState: (innerCustomInitialState: TStateDefault) => void;
ignoreProps: (props: string[] | string) => void;
onChange: (callback: ChangeHandlerFn) => StateMonitor<TStateDefault>;
destroy: () => void;
}
type ChangeHandlerFn = (status: StateStatus, type: string | null, keys: string[]) => void;
function stateMonitor(state: State, customInitialState: State) {
function stateMonitor<TStateDefault extends { [key: string]: unknown }>(
state: State,
customInitialState: TStateDefault
): StateMonitor<TStateDefault> {
let destroyed = false;
let ignoredProps: string[] = [];
let changeHandlers: ChangeHandlerFn[] | undefined = [];
let initialState: State;
let initialState: TStateDefault;
setInitialState(customInitialState);
function setInitialState(innerCustomInitialState: State) {
function setInitialState(innerCustomInitialState: TStateDefault) {
// state.toJSON returns a reference, clone so we can mutate it safely
initialState = cloneDeep(innerCustomInitialState) || cloneDeep(state.toJSON());
}
function removeIgnoredProps(innerState: State) {
function removeIgnoredProps(innerState: TStateDefault) {
ignoredProps.forEach(path => {
set(innerState, path, true);
});
@ -84,7 +97,7 @@ function stateMonitor(state: State, customInitialState: State) {
}
return {
setInitialState(innerCustomInitialState: State) {
setInitialState(innerCustomInitialState: TStateDefault) {
if (!isPlainObject(innerCustomInitialState)) {
throw new TypeError('The default state must be an object');
}
@ -102,7 +115,7 @@ function stateMonitor(state: State, customInitialState: State) {
}
},
ignoreProps(props: string[]) {
ignoreProps(props: string[] | string) {
ignoredProps = ignoredProps.concat(props);
removeIgnoredProps(initialState);
return this;

View file

@ -17,5 +17,44 @@
* under the License.
*/
export type Timefilter = any;
import { Moment } from 'moment';
import { TimeRange } from './time_history';
import moment = require('moment');
// NOTE: These types are somewhat guessed, they may be incorrect.
export interface RefreshInterval {
display?: string;
pause: boolean;
section?: string;
value: number;
}
export interface Timefilter {
time: {
// NOTE: It's unclear if this is supposed to actually allow a moment object, or undefined, or if this is just
// a bug... should be investigated. This should probably be the TimeRange type, but most TimeRange interfaces
// don't account for the possibility of the moment object, and it is actually a possibility.
to: string | moment.Moment | undefined;
from: string | moment.Moment | undefined;
};
getTime: () => {
to: string | moment.Moment | undefined;
from: string | moment.Moment | undefined;
};
setTime: (
timeRange: {
to: string | moment.Moment | undefined;
from: string | moment.Moment | undefined;
}
) => void;
setRefreshInterval: (refreshInterval: RefreshInterval) => void;
getRefreshInterval: () => RefreshInterval;
disableAutoRefreshSelector: () => void;
disableTimeRangeSelector: () => void;
enableAutoRefreshSelector: () => void;
off: (event: string, reload: () => void) => void;
on: (event: string, reload: () => void) => void;
}
export const timefilter: Timefilter;

View file

@ -33,7 +33,7 @@ export interface FilterMeta {
export interface Filter {
meta: FilterMeta;
query: object;
query?: object;
}
export type Filters = Filter[];