Add optional data point markers and mouse-over tooltips to display values on graphs. Fixes #3514

Ensure queries are no longer executed when dashboards are closed. Fixes #3576
This commit is contained in:
Aditya Toshniwal 2018-09-05 17:25:11 +01:00 committed by Dave Page
parent 0e59be237b
commit a74b9c96c1
10 changed files with 324 additions and 63 deletions

View File

@ -11,7 +11,10 @@ Features
******** ********
| `Feature #2927 <https://redmine.postgresql.org/issues/2927>`_ - Move all CSS into SCSS files for consistency and ease of colour maintenance etc. | `Feature #2927 <https://redmine.postgresql.org/issues/2927>`_ - Move all CSS into SCSS files for consistency and ease of colour maintenance etc.
| `Feature #3514 <https://redmine.postgresql.org/issues/3514>`_ - Add optional data point markers and mouse-over tooltips to display values on graphs.
Bug fixes Bug fixes
********* *********
| `Bug #3576 <https://redmine.postgresql.org/issues/3576>`_ - Ensure queries are no longer executed when dashboards are closed.

View File

@ -4,6 +4,7 @@
"axios-mock-adapter": "^1.14.1", "axios-mock-adapter": "^1.14.1",
"babel-core": "~6.24.0", "babel-core": "~6.24.0",
"babel-loader": "~7.1.2", "babel-loader": "~7.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-airbnb": "^2.4.0", "babel-preset-airbnb": "^2.4.0",
"babel-preset-es2015": "~6.24.0", "babel-preset-es2015": "~6.24.0",
"babel-preset-react": "~6.23.0", "babel-preset-react": "~6.23.0",
@ -66,7 +67,7 @@
"dropzone": "^5.1.1", "dropzone": "^5.1.1",
"eonasdan-bootstrap-datetimepicker": "^4.17.47", "eonasdan-bootstrap-datetimepicker": "^4.17.47",
"exports-loader": "~0.6.4", "exports-loader": "~0.6.4",
"flotr2": "^0.1.0", "flotr2": "git+https://github.com/EnterpriseDB/Flotr2.git",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"hard-source-webpack-plugin": "^0.4.9", "hard-source-webpack-plugin": "^0.4.9",
"immutability-helper": "^2.2.0", "immutability-helper": "^2.2.0",

View File

@ -165,7 +165,8 @@ define(
if (eventName == 'panelClosed') { if (eventName == 'panelClosed') {
pgBrowser.save_current_layout(pgBrowser); pgBrowser.save_current_layout(pgBrowser);
pgAdmin.Dashboard.toggleVisibility(false); /* Pass the closed flag also */
pgAdmin.Dashboard.toggleVisibility(false, true);
} else if (eventName == 'panelVisibilityChanged') { } else if (eventName == 'panelVisibilityChanged') {
if (pgBrowser.tree) { if (pgBrowser.tree) {
pgBrowser.save_current_layout(pgBrowser); pgBrowser.save_current_layout(pgBrowser);
@ -174,8 +175,10 @@ define(
pgAdmin.Dashboard.toggleVisibility(pgBrowser.panels.dashboard.panel.isVisible()); pgAdmin.Dashboard.toggleVisibility(pgBrowser.panels.dashboard.panel.isVisible());
} }
// Explicitly trigger tree selected event when we add the tab. // Explicitly trigger tree selected event when we add the tab.
pgBrowser.Events.trigger('pgadmin-browser:tree:selected', selectedNode, if(selectedNode.length) {
pgBrowser.tree.itemData(selectedNode), pgBrowser.Node); pgBrowser.Events.trigger('pgadmin-browser:tree:selected', selectedNode,
pgBrowser.tree.itemData(selectedNode), pgBrowser.Node);
}
} }
} }
}, },

View File

@ -81,7 +81,7 @@ class DashboardModule(PgAdminModule):
help_str=gettext('The number of seconds between graph samples.') help_str=gettext('The number of seconds between graph samples.')
) )
self.session_stats_refresh = self.dashboard_preference.register( self.tps_stats_refresh = self.dashboard_preference.register(
'dashboards', 'tps_stats_refresh', 'dashboards', 'tps_stats_refresh',
gettext("Transaction throughput refresh rate"), 'integer', gettext("Transaction throughput refresh rate"), 'integer',
1, min_val=1, max_val=999999, 1, min_val=1, max_val=999999,
@ -89,7 +89,7 @@ class DashboardModule(PgAdminModule):
help_str=gettext('The number of seconds between graph samples.') help_str=gettext('The number of seconds between graph samples.')
) )
self.session_stats_refresh = self.dashboard_preference.register( self.ti_stats_refresh = self.dashboard_preference.register(
'dashboards', 'ti_stats_refresh', 'dashboards', 'ti_stats_refresh',
gettext("Tuples in refresh rate"), 'integer', gettext("Tuples in refresh rate"), 'integer',
1, min_val=1, max_val=999999, 1, min_val=1, max_val=999999,
@ -97,7 +97,7 @@ class DashboardModule(PgAdminModule):
help_str=gettext('The number of seconds between graph samples.') help_str=gettext('The number of seconds between graph samples.')
) )
self.session_stats_refresh = self.dashboard_preference.register( self.to_stats_refresh = self.dashboard_preference.register(
'dashboards', 'to_stats_refresh', 'dashboards', 'to_stats_refresh',
gettext("Tuples out refresh rate"), 'integer', gettext("Tuples out refresh rate"), 'integer',
1, min_val=1, max_val=999999, 1, min_val=1, max_val=999999,
@ -105,7 +105,7 @@ class DashboardModule(PgAdminModule):
help_str=gettext('The number of seconds between graph samples.') help_str=gettext('The number of seconds between graph samples.')
) )
self.session_stats_refresh = self.dashboard_preference.register( self.bio_stats_refresh = self.dashboard_preference.register(
'dashboards', 'bio_stats_refresh', 'dashboards', 'bio_stats_refresh',
gettext("Block I/O statistics refresh rate"), 'integer', gettext("Block I/O statistics refresh rate"), 'integer',
1, min_val=1, max_val=999999, 1, min_val=1, max_val=999999,
@ -129,6 +129,23 @@ class DashboardModule(PgAdminModule):
'will be displayed on dashboards.') 'will be displayed on dashboards.')
) )
self.graph_data_points = self.dashboard_preference.register(
'display', 'graph_data_points',
gettext("Show graph data points?"), 'boolean', False,
category_label=gettext('Display'),
help_str=gettext('If set to True, data points will be '
'visible on graph lines.')
)
self.graph_mouse_track = self.dashboard_preference.register(
'display', 'graph_mouse_track',
gettext("Show mouse hover tooltip?"), 'boolean', True,
category_label=gettext('Display'),
help_str=gettext('If set to True, tooltip will appear on mouse '
'hover on the graph lines giving the data point '
'details')
)
def get_exposed_url_endpoints(self): def get_exposed_url_endpoints(self):
""" """
Returns: Returns:

View File

@ -0,0 +1,99 @@
import Flotr from 'flotr2';
export class Chart {
constructor(container, options) {
this.chartApi = Flotr;
/* Html Node where the graph goes */
this._container = container;
/* Graph library options */
this._options = {};
this._defaultOptions = {
legend: {
position: 'nw',
backgroundColor: '#D2E8FF',
},
lines: {
show: true,
lineWidth: 2,
},
shadowSize: 0,
resolution : 3,
};
this._dataset = null;
this._tooltipFormatter = null;
/* Just to store other data related to charts. Used nowhere here in the module */
this._otherData = {};
this.setOptions(options);
}
getContainer() {
return this._container;
}
getContainerDimensions() {
return {
height: this._container.clientHeight,
width: this._container.clientWidth,
};
}
getOptions() {
return this._options;
}
/* This should be changed if library changed */
setOptions(options, mergeOptions=true) {
/* If mergeOptions then merge the options, else replace existing options */
if(mergeOptions) {
this._options = {...this._defaultOptions, ...this._options, ...options};
} else {
this._options = {...this._defaultOptions, ...options};
}
}
removeOptions(optionKey) {
if(this._options[optionKey]) {
delete this._options[optionKey];
}
}
getOtherData(key) {
return this._otherData[key];
}
setOtherData(key, value) {
this._otherData[key] = value;
}
isVisible() {
let dim = this.getContainerDimensions();
return (dim.height > 0 && dim.width > 0);
}
isInPage() {
return (this._container === document.body) ? false : document.body.contains(this._container);
}
setTooltipFormatter(tooltipFormatter) {
let opt = this.getOptions();
this._tooltipFormatter = tooltipFormatter;
if(this._tooltipFormatter) {
this.setOptions({
mouse: {
...opt.mouse,
trackFormatter: this._tooltipFormatter,
},
});
}
}
draw(dataset) {
this._dataset = dataset;
if(this._container) {
Flotr.draw(this._container, this._dataset, this._options);
}
}
}

View File

@ -1,11 +1,11 @@
define('pgadmin.dashboard', [ define('pgadmin.dashboard', [
'sources/url_for', 'sources/gettext', 'require', 'jquery', 'underscore', 'sources/url_for', 'sources/gettext', 'require', 'jquery', 'underscore',
'sources/pgadmin', 'backbone', 'backgrid', 'flotr2', 'sources/pgadmin', 'backbone', 'backgrid', './charting',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.alertifyjs', 'pgadmin.backform',
'sources/nodes/dashboard', 'backgrid.filter', 'sources/nodes/dashboard', 'backgrid.filter',
'pgadmin.browser', 'bootstrap', 'wcdocker', 'pgadmin.browser', 'bootstrap', 'wcdocker',
], function( ], function(
url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr, url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, charting,
Alertify, Backform, NodesDashboard Alertify, Backform, NodesDashboard
) { ) {
@ -210,10 +210,8 @@ define('pgadmin.dashboard', [
// Load the default welcome dashboard // Load the default welcome dashboard
var url = url_for('dashboard.index'); var url = url_for('dashboard.index');
/* Store the interval ids of the graph interval functions so that we can clear /* Store the chart objects and there interval ids in this store */
* them when graphs are disabled this.chartStore = {};
*/
this.intervalIds = {};
var dashboardPanel = pgBrowser.panels['dashboard'].panel; var dashboardPanel = pgBrowser.panels['dashboard'].panel;
if (dashboardPanel) { if (dashboardPanel) {
@ -266,7 +264,7 @@ define('pgadmin.dashboard', [
!_.isUndefined(itemData.connected) && !_.isUndefined(itemData.connected) &&
itemData.connected !== true itemData.connected !== true
) { ) {
self.clearIntervalId(); self.clearChartFromStore();
} }
} else if (itemData && itemData._type) { } else if (itemData && itemData._type) {
var treeHierarchy = node.getTreeNodeHierarchy(item), var treeHierarchy = node.getTreeNodeHierarchy(item),
@ -331,8 +329,8 @@ define('pgadmin.dashboard', [
) { ) {
$(div).empty(); $(div).empty();
/* Clear all the interval functions of previous dashboards */ /* Clear all the charts previous dashboards */
self.clearIntervalId(); self.clearChartFromStore();
$.ajax({ $.ajax({
url: url, url: url,
@ -356,8 +354,8 @@ define('pgadmin.dashboard', [
!_.isUndefined(itemData.connected) && !_.isUndefined(itemData.connected) &&
itemData.connected !== true itemData.connected !== true
) { ) {
/* Clear all the interval functions of previous dashboards */ /* Clear all the charts previous dashboards */
self.clearIntervalId(); self.clearChartFromStore();
} }
$(div).html( $(div).html(
'<div class="alert alert-info pg-panel-message" role="alert">' + gettext('Please connect to the selected server to view the dashboard.') + '</div>' '<div class="alert alert-info pg-panel-message" role="alert">' + gettext('Please connect to the selected server to view the dashboard.') + '</div>'
@ -371,7 +369,7 @@ define('pgadmin.dashboard', [
} }
}, },
renderChartLoop: function(container, sid, did, url, options, counter, refresh) { renderChartLoop: function(chartObj, sid, did, url, counter, refresh) {
var data = [], var data = [],
dataset = []; dataset = [];
@ -386,24 +384,22 @@ define('pgadmin.dashboard', [
dataType: 'html', dataType: 'html',
}) })
.done(function(resp) { .done(function(resp) {
$(container).removeClass('graph-error'); $(chartObj.getContainer()).removeClass('graph-error');
data = JSON.parse(resp); data = JSON.parse(resp);
if (!dashboardVisible)
return;
var y = 0, var y = 0,
x; x;
if (dataset.length == 0) { if (dataset.length == 0) {
if (counter == true) { if (counter == true) {
// Have we stashed initial values? // Have we stashed initial values?
if (_.isUndefined($(container).data('counter_previous_vals'))) { if (_.isUndefined(chartObj.getOtherData('counter_previous_vals'))) {
$(container).data('counter_previous_vals', data[0]); chartObj.setOtherData('counter_previous_vals', data[0]);
} else { } else {
// Create the initial data structure // Create the initial data structure
for (x in data[0]) { for (x in data[0]) {
dataset.push({ dataset.push({
'data': [ 'data': [
[0, data[0][x] - $(container).data('counter_previous_vals')[x]], [0, data[0][x] - chartObj.getOtherData('counter_previous_vals')[x]],
], ],
'label': x, 'label': x,
}); });
@ -429,10 +425,10 @@ define('pgadmin.dashboard', [
} else { } else {
// Store the current value, minus the previous one we stashed. // Store the current value, minus the previous one we stashed.
// It's possible the tab has been reloaded, in which case out previous values are gone // It's possible the tab has been reloaded, in which case out previous values are gone
if (_.isUndefined($(container).data('counter_previous_vals'))) if (_.isUndefined(chartObj.getOtherData('counter_previous_vals')))
return; return;
dataset[y]['data'].unshift([0, data[0][x] - $(container).data('counter_previous_vals')[x]]); dataset[y]['data'].unshift([0, data[0][x] - chartObj.getOtherData('counter_previous_vals')[x]]);
} }
// Reset the time index to get a proper scrolling display // Reset the time index to get a proper scrolling display
@ -442,7 +438,7 @@ define('pgadmin.dashboard', [
y++; y++;
} }
$(container).data('counter_previous_vals', data[0]); chartObj.setOtherData('counter_previous_vals', data[0]);
} }
// Remove uneeded elements // Remove uneeded elements
@ -453,12 +449,9 @@ define('pgadmin.dashboard', [
} }
} }
// Draw Graph, if the container still exists and has a size if (chartObj.isInPage()) {
var dashboardPanel = pgBrowser.panels['dashboard'].panel; if (chartObj.isVisible()) {
var div = dashboardPanel.layout().scene().find('.pg-panel-content'); chartObj.draw(dataset);
if ($(div).find(container).length) { // Exists?
if (container.clientHeight > 0 && container.clientWidth > 0) { // Not hidden?
Flotr.draw(container, dataset, options);
} }
} else { } else {
return; return;
@ -487,8 +480,8 @@ define('pgadmin.dashboard', [
} }
} }
$(container).addClass('graph-error'); $(chartObj.getContainer()).addClass('graph-error');
$(container).html( $(chartObj.getContainer()).html(
'<div class="alert alert-' + cls + ' pg-panel-message" role="alert">' + msg + '</div>' '<div class="alert alert-' + cls + ' pg-panel-message" role="alert">' + msg + '</div>'
); );
}); });
@ -510,15 +503,41 @@ define('pgadmin.dashboard', [
// { data: [[0, y0], [1, y1]...], label: 'Label 3', [options] } // { data: [[0, y0], [1, y1]...], label: 'Label 3', [options] }
// ] // ]
let self = this; let self = this,
if(self.intervalIds[chartName] tooltipFormatter = function(refresh, currVal) {
return(`Seconds ago: ${parseInt(currVal.x * refresh)}</br>
Value: ${currVal.y}`);
};
if(self.chartStore[chartName]
&& self.old_preferences[prefName] != self.preferences[prefName]) { && self.old_preferences[prefName] != self.preferences[prefName]) {
self.clearIntervalId(chartName); self.clearChartFromStore(chartName);
} }
if(!self.intervalIds[chartName]) {
self.intervalIds[chartName] = self.renderChartLoop( if(self.chartStore[chartName]) {
container, self.sid, self.did, url, let chartObj = self.chartStore[chartName].chartObj;
options, counter, self.preferences[prefName] chartObj.setOptions(options, false);
chartObj.setTooltipFormatter(
tooltipFormatter.bind(null, self.preferences[prefName])
);
}
if(!self.chartStore[chartName]) {
let chartObj = new charting.Chart(container, options);
chartObj.setTooltipFormatter(
tooltipFormatter.bind(null, self.preferences[prefName])
);
self.chartStore[chartName] = {
'chartObj' : chartObj,
'intervalId' : undefined,
};
self.chartStore[chartName]['intervalId'] = self.renderChartLoop(
self.chartStore[chartName]['chartObj'], self.sid, self.did, url,
counter, self.preferences[prefName]
); );
} }
}, },
@ -666,17 +685,17 @@ define('pgadmin.dashboard', [
}); });
}, },
clearIntervalId: function(intervalId) { clearChartFromStore: function(chartName) {
var self = this; var self = this;
if(!intervalId){ if(!chartName){
_.each(self.intervalIds, function(id, key) { _.each(self.chartStore, function(chart, key) {
clearInterval(id); clearInterval(chart.intervalId);
delete self.intervalIds[key]; delete self.chartStore[key];
}); });
} }
else { else {
clearInterval(self.intervalIds[intervalId]); clearInterval(self.chartStore[chartName].intervalId);
delete self.intervalIds[intervalId]; delete self.chartStore[chartName];
} }
}, },
@ -737,20 +756,41 @@ define('pgadmin.dashboard', [
yaxis: { yaxis: {
autoscale: 1, autoscale: 1,
}, },
legend: {
position: 'nw',
backgroundColor: '#D2E8FF',
},
shadowSize: 0,
resolution : 5,
}; };
if(self.preferences.graph_data_points) {
/* Merge data points related options */
options_line = {
...options_line,
...{
points: {
show:true,
radius: 1,
hitRadius: 3,
},
},
};
}
if(self.preferences.graph_mouse_track) {
/* Merge mouse track related options */
options_line = {
...options_line,
...{
mouse: {
track:true,
position: 'sw',
},
},
};
}
if(self.preferences.show_graphs && $('#dashboard-graphs').hasClass('dashboard-hidden')) { if(self.preferences.show_graphs && $('#dashboard-graphs').hasClass('dashboard-hidden')) {
$('#dashboard-graphs').removeClass('dashboard-hidden'); $('#dashboard-graphs').removeClass('dashboard-hidden');
} }
else if(!self.preferences.show_graphs) { else if(!self.preferences.show_graphs) {
$('#dashboard-graphs').addClass('dashboard-hidden'); $('#dashboard-graphs').addClass('dashboard-hidden');
self.clearIntervalId(); self.clearChartFromStore();
} }
if (self.preferences.show_activity && $('#dashboard-activity').hasClass('dashboard-hidden')) { if (self.preferences.show_activity && $('#dashboard-activity').hasClass('dashboard-hidden')) {
@ -1344,8 +1384,11 @@ define('pgadmin.dashboard', [
}); });
} }
}, },
toggleVisibility: function(flag) { toggleVisibility: function(visible, closed=false) {
dashboardVisible = flag; dashboardVisible = visible;
if(closed) {
this.clearChartFromStore();
}
}, },
can_take_action: function(m) { can_take_action: function(m) {
// We will validate if user is allowed to cancel the active query // We will validate if user is allowed to cancel the active query

View File

@ -0,0 +1,91 @@
import $ from 'jquery';
import {Chart} from 'top/dashboard/static/js/charting';
describe('In charting related testcases', ()=> {
let chartObj = undefined,
chartDiv = undefined,
options = {};
beforeEach(()=> {
$('body').append(
'<div id="charting-test-container"></div>'
);
chartDiv = $('#charting-test-container')[0];
chartObj = new Chart(chartDiv, options);
});
it('Chart api should be defined', ()=>{
expect(chartObj.chartApi).toBeDefined();
});
it('Return the correct container', ()=>{
expect(chartObj.getContainer()).toBe(chartDiv);
});
it('Returns the container dimensions', ()=>{
let dim = chartObj.getContainerDimensions();
expect(dim.height).toBeDefined();
expect(dim.width).toBeDefined();
});
it('Check if options are set', ()=>{
chartObj.setOptions({
mouse: {
track:true,
},
});
let opt = chartObj.getOptions();
expect(opt.mouse).toBeDefined();
});
it('Check if options are set with mergeOptions false', ()=>{
let overOpt = {
mouse: {
track:true,
},
};
chartObj.setOptions(overOpt, false);
let newOptShouldBe = {...chartObj._defaultOptions, ...overOpt};
let opt = chartObj.getOptions();
expect(JSON.stringify(opt)).toEqual(JSON.stringify(newOptShouldBe));
});
it('Check if other data is set', ()=>{
chartObj.setOtherData('some_val', 1);
expect(chartObj._otherData['some_val']).toEqual(1);
});
it('Check if other data is get', ()=>{
chartObj.setOtherData('some_val', 1);
expect(chartObj.getOtherData('some_val')).toEqual(1);
});
it('Check if isVisible returns correct', ()=>{
let dimSpy = spyOn(chartObj, 'getContainerDimensions');
dimSpy.and.returnValue({
height: 1, width: 1,
});
expect(chartObj.isVisible()).toBe(true);
dimSpy.and.stub();
dimSpy.and.returnValue({
height: 0, width: 0,
});
expect(chartObj.isVisible()).toBe(false);
});
it('Check if isInPage returns correct', ()=>{
expect(chartObj.isInPage()).toBe(true);
$('body').find('#charting-test-container').remove();
expect(chartObj.isInPage()).toBe(false);
});
afterEach(()=>{
$('body').find('#charting-test-container').remove();
});
});

View File

@ -168,6 +168,7 @@ module.exports = {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: ['es2015', 'react'], presets: ['es2015', 'react'],
plugins: ['transform-object-rest-spread'],
}, },
}, },
}, { }, {

View File

@ -58,6 +58,7 @@ module.exports = {
resolve: { resolve: {
extensions: ['.js', '.jsx'], extensions: ['.js', '.jsx'],
alias: { alias: {
'top': path.join(__dirname, './pgadmin'),
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'), 'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
'alertify': path.join(__dirname, './node_modules/alertifyjs/build/alertify'), 'alertify': path.join(__dirname, './node_modules/alertifyjs/build/alertify'),
'jquery.event.drag': path.join(__dirname, './node_modules/slickgrid/lib/jquery.event.drag-2.3.0'), 'jquery.event.drag': path.join(__dirname, './node_modules/slickgrid/lib/jquery.event.drag-2.3.0'),
@ -75,6 +76,8 @@ module.exports = {
'slickgrid': nodeModulesDir + '/slickgrid/', 'slickgrid': nodeModulesDir + '/slickgrid/',
'slickgrid.plugins': nodeModulesDir + '/slickgrid/plugins/', 'slickgrid.plugins': nodeModulesDir + '/slickgrid/plugins/',
'slickgrid.grid': nodeModulesDir + '/slickgrid/slick.grid', 'slickgrid.grid': nodeModulesDir + '/slickgrid/slick.grid',
'bean': path.join(__dirname, './node_modules/flotr2/lib/bean'),
'flotr2': path.join(__dirname, './node_modules/flotr2/flotr2.amd'),
'browser': path.resolve(__dirname, 'pgadmin/browser/static/js'), 'browser': path.resolve(__dirname, 'pgadmin/browser/static/js'),
'pgadmin': sourcesDir + '/js/pgadmin', 'pgadmin': sourcesDir + '/js/pgadmin',
'pgadmin.sqlfoldcode': sourcesDir + '/js/codemirror/addon/fold/pgadmin-sqlfoldcode', 'pgadmin.sqlfoldcode': sourcesDir + '/js/codemirror/addon/fold/pgadmin-sqlfoldcode',

View File

@ -907,7 +907,7 @@ babel-plugin-transform-jscript@^6.22.0:
dependencies: dependencies:
babel-runtime "^6.22.0" babel-runtime "^6.22.0"
babel-plugin-transform-object-rest-spread@^6.23.0: babel-plugin-transform-object-rest-spread@^6.23.0, babel-plugin-transform-object-rest-spread@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
dependencies: dependencies:
@ -3766,9 +3766,9 @@ flatten@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
flotr2@^0.1.0: "flotr2@git+https://github.com/EnterpriseDB/Flotr2.git":
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/flotr2/-/flotr2-0.1.0.tgz#8d31b0d1b3dc46f5e399edeb7a179e075b36e036" resolved "git+https://github.com/EnterpriseDB/Flotr2.git#dc514e32d93b2c593ab379a52de1664ee9da63e1"
flush-write-stream@^1.0.2: flush-write-stream@^1.0.2:
version "1.0.3" version "1.0.3"