mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
merging in upstream vislib/refactor
This commit is contained in:
commit
acdc482e45
6 changed files with 399 additions and 119 deletions
|
@ -48,18 +48,14 @@ visualize {
|
|||
}
|
||||
|
||||
.y-axis-div {
|
||||
flex: 5 1;
|
||||
min-width: 35px
|
||||
flex: 4 1;
|
||||
min-width: 25px
|
||||
}
|
||||
|
||||
.y-axis-filler-div {
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
//.legend-wrapper {
|
||||
// flex: 1 1;
|
||||
//}
|
||||
|
||||
div.x-axis-label {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
@ -82,21 +78,30 @@ div.y-axis-label {
|
|||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
/* legends */
|
||||
/* legend */
|
||||
.legend-col-wrapper {
|
||||
flex: 0.3 1 auto;
|
||||
z-index: 10;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
min-width: 40px
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
margin: 0 0 14px 0;
|
||||
}
|
||||
|
||||
.legend-ul {
|
||||
list-style-type: none;
|
||||
margin: 0 0 0 10px;
|
||||
margin: 0 0 0 14px;
|
||||
padding: 0;
|
||||
visibility: visible;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
.legend-ul.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
@ -255,9 +260,9 @@ path, line, .axis line, .axis path {
|
|||
/* YAxis logic */
|
||||
.y-axis-col-wrapper {
|
||||
display: flex;
|
||||
flex: 2 1;
|
||||
flex: 3 1;
|
||||
flex-direction: column;
|
||||
min-width: 50px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.y-axis-col {
|
||||
|
@ -353,7 +358,7 @@ path, line, .axis line, .axis path {
|
|||
flex: 8 1;
|
||||
flex-direction: row;
|
||||
// overflow: visible;
|
||||
min-height: 16px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
@ -382,14 +387,6 @@ path, line, .axis line, .axis path {
|
|||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.legend-col-wrapper {
|
||||
flex: 0 1 auto;
|
||||
z-index: 10;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
min-width: 30px
|
||||
}
|
||||
|
||||
.x.axis path {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -17,9 +17,9 @@ define(function (require) {
|
|||
return new Handler(vis);
|
||||
}
|
||||
|
||||
this.data = new Data(vis.data);
|
||||
this.vis = vis;
|
||||
this.el = vis.el;
|
||||
this.data = new Data(vis.data);
|
||||
this.ChartClass = vis.ChartClass;
|
||||
this._attr = _.defaults(vis._attr || {}, {
|
||||
'margin' : { top: 10, right: 3, bottom: 5, left: 3 },
|
||||
|
|
|
@ -20,6 +20,8 @@ define(function (require) {
|
|||
|
||||
XAxis.prototype.render = function () {
|
||||
d3.select(this.el).selectAll('.x-axis-div').call(this.draw());
|
||||
d3.select(this.el).selectAll('.x-axis-div').call(this.checkTickLabels());
|
||||
d3.select(this.el).selectAll('.x-axis-div').call(this.resizeAxisLayoutForLabels());
|
||||
};
|
||||
|
||||
XAxis.prototype.getScale = function (ordered) {
|
||||
|
@ -104,8 +106,8 @@ define(function (require) {
|
|||
} else {
|
||||
self._attr.isDiscover = false;
|
||||
}
|
||||
|
||||
return function (selection) {
|
||||
|
||||
selection.each(function () {
|
||||
div = d3.select(this);
|
||||
width = $(this).width() - margin.left - margin.right;
|
||||
|
@ -125,9 +127,6 @@ define(function (require) {
|
|||
.attr('transform', 'translate(' + margin.left + ',0)')
|
||||
.call(self.xAxis);
|
||||
});
|
||||
|
||||
self.checkTickLabels(selection);
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -151,57 +150,58 @@ define(function (require) {
|
|||
var rotate = false;
|
||||
self._attr.isRotated = false;
|
||||
|
||||
selection.each(function () {
|
||||
// select each x axis
|
||||
div = d3.select(this);
|
||||
width = $(this).width() - margin.left - margin.right;
|
||||
height = $(this).height();
|
||||
labels = selection.selectAll('.tick text');
|
||||
return function (selection) {
|
||||
|
||||
// total widths for every label from each x axis
|
||||
subtotalW = 0;
|
||||
subtotalH = 0;
|
||||
_.forEach(labels[0], function (n) {
|
||||
subtotalW += n.getBBox().width;
|
||||
subtotalH += n.getBBox().height;
|
||||
selection.each(function () {
|
||||
// select each x axis
|
||||
div = d3.select(this);
|
||||
width = $(this).width() - margin.left - margin.right;
|
||||
height = $(this).height();
|
||||
labels = selection.selectAll('.tick text');
|
||||
|
||||
// total widths for every label from each x axis
|
||||
subtotalW = 0;
|
||||
subtotalH = 0;
|
||||
_.forEach(labels[0], function (n) {
|
||||
subtotalW += n.getBBox().width;
|
||||
subtotalH += n.getBBox().height;
|
||||
});
|
||||
|
||||
widthArr.push(subtotalW);
|
||||
heightArr.push(subtotalH);
|
||||
|
||||
// should rotate if any chart subtotal > width
|
||||
if (subtotalW > width) {
|
||||
rotate = true;
|
||||
}
|
||||
});
|
||||
|
||||
widthArr.push(subtotalW);
|
||||
heightArr.push(subtotalH);
|
||||
|
||||
// should rotate if any chart subtotal > width
|
||||
if (subtotalW > width) {
|
||||
rotate = true;
|
||||
// apply rotate if not discover view
|
||||
if (!self._attr.isDiscover && rotate) {
|
||||
self.rotateAxisLabels(selection);
|
||||
self._attr.isRotated = true;
|
||||
}
|
||||
});
|
||||
|
||||
// apply rotate if not discover view
|
||||
if (!self._attr.isDiscover && rotate) {
|
||||
self.rotateAxisLabels(selection);
|
||||
self._attr.isRotated = true;
|
||||
}
|
||||
|
||||
// filter labels to prevent overlap of text
|
||||
if (self._attr.isRotated) {
|
||||
// if rotated, use label heights
|
||||
maxHeight = _.max(heightArr);
|
||||
total = _.reduce(heightArr, function (sum, n) {
|
||||
return sum + (n * 1.05);
|
||||
});
|
||||
nth = 1 + Math.floor(maxHeight / width);
|
||||
} else {
|
||||
// if not rotated, use label widths
|
||||
maxWidth = _.max(widthArr);
|
||||
total = _.reduce(widthArr, function (sum, n) {
|
||||
return sum + (n * 1.05);
|
||||
});
|
||||
nth = 1 + Math.floor(maxWidth / width);
|
||||
}
|
||||
if (nth > 1) {
|
||||
self.filterAxisLabels(selection, nth);
|
||||
}
|
||||
|
||||
//self.resizeAxisLayoutForLabels(selection);
|
||||
|
||||
// filter labels to prevent overlap of text
|
||||
if (self._attr.isRotated) {
|
||||
// if rotated, use label heights
|
||||
maxHeight = _.max(heightArr);
|
||||
total = _.reduce(heightArr, function (sum, n) {
|
||||
return sum + (n * 1.05);
|
||||
});
|
||||
nth = 1 + Math.floor(maxHeight / width);
|
||||
} else {
|
||||
// if not rotated, use label widths
|
||||
maxWidth = _.max(widthArr);
|
||||
total = _.reduce(widthArr, function (sum, n) {
|
||||
return sum + (n * 1.05);
|
||||
});
|
||||
nth = 1 + Math.floor(maxWidth / width);
|
||||
}
|
||||
if (nth > 1) {
|
||||
self.filterAxisLabels(selection, nth);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
XAxis.prototype.rotateAxisLabels = function (selection) {
|
||||
|
@ -237,57 +237,61 @@ define(function (require) {
|
|||
var ratio;
|
||||
var flex;
|
||||
var chartToXaxis;
|
||||
var dataType = selection[0][0].__data__.rows ? 'rows' : 'columns';
|
||||
var dataType;
|
||||
|
||||
var rotScale = d3.scale.linear()
|
||||
.domain([0.12, 0.3, 0.7, 2.1])
|
||||
.range([4.3, 13, 27, 102]);
|
||||
return function (selection) {
|
||||
var rotScale = d3.scale.linear()
|
||||
.domain([0.14, 0.28, 0.6, 0.8, 0.9, 2.6])
|
||||
.range([5, 10, 30, 55, 80, 100]);
|
||||
var flatScale = d3.scale.linear()
|
||||
.domain([1.3, 8, 14.5, 29])
|
||||
.range([1.1, 9, 13, 30]);
|
||||
|
||||
var flatScale = d3.scale.linear()
|
||||
.domain([2.2, 14.5])
|
||||
.range([1.2, 11]);
|
||||
selection.each(function () {
|
||||
div = d3.select(this);
|
||||
svg = div.select('svg');
|
||||
tick = svg.select('.tick');
|
||||
dataType = this.parentNode.__data__.series ? 'series' : this.parentNode.__data__.rows ? 'rows' : 'columns';
|
||||
|
||||
selection.each(function () {
|
||||
xwrapper = visEl.find('.x-axis-wrapper');
|
||||
xdiv = visEl.find('.x-axis-div');
|
||||
xdivwrapper = visEl.find('.x-axis-div-wrapper');
|
||||
yspacerblock = visEl.find('.y-axis-spacer-block');
|
||||
|
||||
div = d3.select(this);
|
||||
svg = div.select('svg');
|
||||
tick = svg.select('.tick');
|
||||
if (dataType === 'series') {
|
||||
chartwrap = visEl.find('.chart-wrapper');
|
||||
titlespace = 15;
|
||||
} else if (dataType === 'rows') {
|
||||
chartwrap = visEl.find('.chart-wrapper-row');
|
||||
titlespace = 15;
|
||||
} else {
|
||||
chartwrap = visEl.find('.chart-wrapper-column');
|
||||
titlespace = 30;
|
||||
}
|
||||
|
||||
if (dataType === 'rows') {
|
||||
chartwrap = visEl.find('.chart-wrapper-row');
|
||||
titlespace = 10;
|
||||
} else {
|
||||
chartwrap = visEl.find('.chart-wrapper-column');
|
||||
titlespace = 28;
|
||||
}
|
||||
xwrapper = visEl.find('.x-axis-wrapper');
|
||||
xdiv = visEl.find('.x-axis-div');
|
||||
xdivwrapper = visEl.find('.x-axis-div-wrapper');
|
||||
yspacerblock = visEl.find('.y-axis-spacer-block');
|
||||
if (!self._attr.isRotated) {
|
||||
// flat labels
|
||||
ratio = flatScale(1800 / chartwrap.height());
|
||||
xdivwrapper.css('flex', flex + ' 1');
|
||||
//console.log('FLAT:', ratio);
|
||||
//console.log(chartwrap.height());
|
||||
} else {
|
||||
// rotated labels
|
||||
ratio = rotScale((titlespace + tick.node().getBBox().height) / chartwrap.height());
|
||||
div.style('height', 2 + tick.node().getBBox().height + 'px');
|
||||
svg.attr('height', 2 + tick.node().getBBox().height + 'px');
|
||||
xdivwrapper.css('min-height', tick.node().getBBox().height);
|
||||
//console.log('ROT:', ratio);
|
||||
//console.log((titlespace + tick.node().getBBox().height) / chartwrap.height());
|
||||
}
|
||||
if (!self._attr.isRotated) {
|
||||
// flat labels
|
||||
ratio = flatScale(35 * (titlespace +
|
||||
tick.node().getBBox().height) / chartwrap.height());
|
||||
//console.log('FLAT:', ratio);
|
||||
//console.log(35 * (titlespace + tick.node().getBBox().height) / chartwrap.height());
|
||||
} else {
|
||||
// rotated labels
|
||||
ratio = rotScale((titlespace + tick.node().getBBox().height) / chartwrap.height());
|
||||
div.style('height', 2 + tick.node().getBBox().height + 'px');
|
||||
svg.attr('height', 2 + tick.node().getBBox().height + 'px');
|
||||
//console.log('ROT:', ratio);
|
||||
//console.log((titlespace + tick.node().getBBox().height) / chartwrap.height());
|
||||
}
|
||||
|
||||
flex = ratio.toFixed(1);
|
||||
xwrapper.css('flex', flex + ' 1');
|
||||
xdiv.css('flex', flex + ' 1');
|
||||
yspacerblock.css('flex', flex + ' 1');
|
||||
//console.log('flex:', flex);
|
||||
|
||||
});
|
||||
flex = ratio.toFixed(1);
|
||||
xwrapper.css('flex', flex + ' 1');
|
||||
xdiv.css('flex', flex + ' 1');
|
||||
yspacerblock.css('flex', flex + ' 1');
|
||||
//console.log('flex:', flex);
|
||||
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return XAxis;
|
||||
|
|
|
@ -101,6 +101,7 @@
|
|||
'specs/vislib/vis',
|
||||
'specs/vislib/handler',
|
||||
'specs/vislib/_error_handler',
|
||||
'specs/vislib/data',
|
||||
'specs/utils/diff_time_picker_vals',
|
||||
'specs/factories/events',
|
||||
'specs/index_patterns/_flatten_search_response',
|
||||
|
|
251
test/unit/specs/vislib/data.js
Normal file
251
test/unit/specs/vislib/data.js
Normal file
|
@ -0,0 +1,251 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
|
||||
var seriesData = {
|
||||
'label': '',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var rowsData = {
|
||||
'rows': [
|
||||
{
|
||||
'label': 'a',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'b',
|
||||
'series': [
|
||||
{
|
||||
'label': '300',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'c',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'd',
|
||||
'series': [
|
||||
{
|
||||
'label': '200',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var colsData = {
|
||||
'columns': [
|
||||
{
|
||||
'label': 'a',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'b',
|
||||
'series': [
|
||||
{
|
||||
'label': '300',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'c',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'd',
|
||||
'series': [
|
||||
{
|
||||
'label': '200',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var seriesData2 = {
|
||||
'label': '',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
},
|
||||
{
|
||||
'label': '200',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var rowsData2 = {
|
||||
'rows': [
|
||||
{
|
||||
'label': 'a',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
},
|
||||
{
|
||||
'label': '200',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'b',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
},
|
||||
{
|
||||
'label': '200',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var colsData2 = {
|
||||
'columns': [
|
||||
{
|
||||
'label': 'a',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
},
|
||||
{
|
||||
'label': '200',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'b',
|
||||
'series': [
|
||||
{
|
||||
'label': '100',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
},
|
||||
{
|
||||
'label': '200',
|
||||
'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var flattenedData = [
|
||||
[{x: 0, y: 0}, {x: 1, y: 2}, {x: 2, y: 4}, {x: 3, y: 6}, {x: 4, y: 8}],
|
||||
[{x: 0, y: 0}, {x: 1, y: 2}, {x: 2, y: 4}, {x: 3, y: 6}, {x: 4, y: 8}],
|
||||
[{x: 0, y: 0}, {x: 1, y: 2}, {x: 2, y: 4}, {x: 3, y: 6}, {x: 4, y: 8}]
|
||||
];
|
||||
|
||||
angular.module('DataFactory', ['kibana']);
|
||||
|
||||
describe('Vislib Data Class Test Suite', function () {
|
||||
|
||||
describe('Data Class (main)', function () {
|
||||
var dataFactory;
|
||||
var rowIn;
|
||||
|
||||
beforeEach(function () {
|
||||
module('DataFactory');
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
inject(function (d3, Private) {
|
||||
dataFactory = Private(require('components/vislib/modules/Data'));
|
||||
});
|
||||
rowIn = new dataFactory(rowsData);
|
||||
});
|
||||
|
||||
it('should be a function', function () {
|
||||
expect(_.isFunction(dataFactory)).to.be(true);
|
||||
});
|
||||
|
||||
it('should return an object', function () {
|
||||
expect(_.isObject(rowIn)).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Data.flatten', function () {
|
||||
var dataFactory;
|
||||
var serIn;
|
||||
var rowIn;
|
||||
var colIn;
|
||||
var serOut;
|
||||
var rowOut;
|
||||
var colOut;
|
||||
|
||||
beforeEach(function () {
|
||||
module('DataFactory');
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
inject(function (d3, Private) {
|
||||
dataFactory = Private(require('components/vislib/modules/Data'));
|
||||
});
|
||||
serIn = new dataFactory(seriesData);
|
||||
rowIn = new dataFactory(rowsData);
|
||||
colIn = new dataFactory(colsData);
|
||||
serOut = serIn.flatten();
|
||||
rowOut = rowIn.flatten();
|
||||
colOut = colIn.flatten();
|
||||
});
|
||||
|
||||
it('should return an array of arrays', function () {
|
||||
expect(_.isArray(serOut)).to.be(true);
|
||||
});
|
||||
|
||||
it('should return array length 3', function () {
|
||||
expect(serOut[0][0].length).to.be(3);
|
||||
});
|
||||
|
||||
it('should return array length 3', function () {
|
||||
expect(rowOut[0][0].length).to.be(3);
|
||||
});
|
||||
|
||||
it('should return array length 3', function () {
|
||||
expect(colOut[0][0].length).to.be(3);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -80,9 +80,9 @@ define(function (require) {
|
|||
|
||||
beforeEach(function () {
|
||||
inject(function (d3, Private) {
|
||||
XAxis = Private(require('components/vislib/modules/Xaxis'));
|
||||
Data = Private(require('components/vislib/modules/Data'));
|
||||
|
||||
XAxis = Private(require('components/vislib/modules/Xaxis'));
|
||||
|
||||
el = d3.select('body').append('div')
|
||||
.attr('class', 'x-axis-wrapper');
|
||||
|
||||
|
@ -91,8 +91,9 @@ define(function (require) {
|
|||
.style('height', '20px');
|
||||
|
||||
dataObj = new Data(data);
|
||||
|
||||
xAxis = new XAxis({
|
||||
el: $('.x-axis-wrapper')[0],
|
||||
el: $('x-axis-div')[0],
|
||||
xValues: dataObj.xValues(),
|
||||
ordered: dataObj.get('ordered'),
|
||||
xAxisFormatter: dataObj.get('xAxisFormatter'),
|
||||
|
@ -226,5 +227,31 @@ define(function (require) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('checkTickLabels Method', function () {
|
||||
var selection;
|
||||
|
||||
beforeEach(function () {
|
||||
xAxis.render();
|
||||
selection = $('.x-axis-wrapper');
|
||||
});
|
||||
|
||||
it('should be a function', function () {
|
||||
expect(_.isFunction(xAxis.checkTickLabels(selection))).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resizeAxisLayoutForLabels Method', function () {
|
||||
var selection;
|
||||
|
||||
beforeEach(function () {
|
||||
xAxis.render();
|
||||
selection = $('.x-axis-wrapper');
|
||||
});
|
||||
|
||||
it('should be a function', function () {
|
||||
expect(_.isFunction(xAxis.checkTickLabels(selection))).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue