IGNITE-9105 Web Console: Added support of canvas based charts.
Project: http://git-wip-us.apache.org/repos/asf/ignite/repo Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/4d9f4504 Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/4d9f4504 Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/4d9f4504 Branch: refs/heads/ignite-8446 Commit: 4d9f45041d8cb1ece1e2612d265fa3b792246857 Parents: 1b5d275 Author: Alexander Kalinin <verba...@yandex.ru> Authored: Tue Jul 31 10:24:32 2018 +0700 Committer: Alexey Kuznetsov <akuznet...@apache.org> Committed: Tue Jul 31 10:24:32 2018 +0700 ---------------------------------------------------------------------- modules/web-console/frontend/app/app.js | 4 + .../ignite-chart-series-selector/component.js | 28 ++ .../ignite-chart-series-selector/controller.js | 62 ++++ .../ignite-chart-series-selector/index.js | 24 ++ .../ignite-chart-series-selector/template.pug | 29 ++ .../app/components/ignite-chart/controller.js | 320 +++++++++++++++++++ .../app/components/ignite-chart/index.js | 38 +++ .../app/components/ignite-chart/style.scss | 69 ++++ .../app/components/ignite-chart/template.pug | 36 +++ .../frontend/app/primitives/btn/index.scss | 9 + modules/web-console/frontend/package.json | 2 + 11 files changed, 621 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/app.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/app.js b/modules/web-console/frontend/app/app.js index baf06dd..e807319 100644 --- a/modules/web-console/frontend/app/app.js +++ b/modules/web-console/frontend/app/app.js @@ -142,6 +142,8 @@ import pageLanding from './components/page-landing'; import passwordVisibility from './components/password-visibility'; import progressLine from './components/progress-line'; import formField from './components/form-field'; +import igniteChart from './components/ignite-chart'; +import igniteChartSelector from './components/ignite-chart-series-selector'; import pageProfile from './components/page-profile'; import pagePasswordChanged from './components/page-password-changed'; @@ -249,6 +251,8 @@ export default angular.module('ignite-console', [ uiAceSpring.name, breadcrumbs.name, passwordVisibility.name, + igniteChart.name, + igniteChartSelector.name, progressLine.name, formField.name ]) http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/components/ignite-chart-series-selector/component.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/ignite-chart-series-selector/component.js b/modules/web-console/frontend/app/components/ignite-chart-series-selector/component.js new file mode 100644 index 0000000..41549fc --- /dev/null +++ b/modules/web-console/frontend/app/components/ignite-chart-series-selector/component.js @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 template from './template.pug'; +import controller from './controller.js'; + +export default { + template, + controller, + transclude: true, + bindings: { + chartApi: '<' + } +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/components/ignite-chart-series-selector/controller.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/ignite-chart-series-selector/controller.js b/modules/web-console/frontend/app/components/ignite-chart-series-selector/controller.js new file mode 100644 index 0000000..00f57ca --- /dev/null +++ b/modules/web-console/frontend/app/components/ignite-chart-series-selector/controller.js @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default class IgniteChartSeriesSelectorController { + static $inject = []; + + constructor() { + this.charts = []; + this.selectedCharts = []; + } + + $onChanges(changes) { + if (changes && 'chartApi' in changes && changes.chartApi.currentValue) + this.applyValues(); + } + + applyValues() { + this.charts = this._makeMenu(); + this.selectedCharts = this.charts.map(({ key }) => key); + } + + setSelectedCharts() { + const selectedDataset = ({ label }) => this.selectedCharts.includes(label); + + this.chartApi.config.data.datasets + .forEach((dataset) => { + dataset.hidden = true; + + if (!selectedDataset(dataset)) + return; + + dataset.hidden = false; + }); + + this.chartApi.update(); + } + + _makeMenu() { + const labels = this.chartApi.config.datasetLegendMapping; + + return Object.keys(this.chartApi.config.datasetLegendMapping).map((key) => { + return { + key, + label: labels[key] + }; + }); + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/components/ignite-chart-series-selector/index.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/ignite-chart-series-selector/index.js b/modules/web-console/frontend/app/components/ignite-chart-series-selector/index.js new file mode 100644 index 0000000..7ad3da0 --- /dev/null +++ b/modules/web-console/frontend/app/components/ignite-chart-series-selector/index.js @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import angular from 'angular'; + +import component from './component'; + +export default angular + .module('ignite-console.ignite-chart-series-selector', []) + .component('igniteChartSeriesSelector', component); http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/components/ignite-chart-series-selector/template.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/ignite-chart-series-selector/template.pug b/modules/web-console/frontend/app/components/ignite-chart-series-selector/template.pug new file mode 100644 index 0000000..fec0d9a --- /dev/null +++ b/modules/web-console/frontend/app/components/ignite-chart-series-selector/template.pug @@ -0,0 +1,29 @@ +//- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF 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. + +button.btn-ignite.btn-ignite--link-dashed-secondary( + protect-from-bs-select-render + bs-select + ng-model='$ctrl.selectedCharts' + ng-change='$ctrl.setSelectedCharts()' + ng-model-options='{debounce: {default: 5}}', + bs-options='chart.key as chart.label for chart in $ctrl.charts' + bs-on-before-show='$ctrl.onShow' + data-multiple='true' + ng-transclude + ng-show='$ctrl.charts.length' +) + svg(ignite-icon='gear').icon http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/components/ignite-chart/controller.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/ignite-chart/controller.js b/modules/web-console/frontend/app/components/ignite-chart/controller.js new file mode 100644 index 0000000..df11050 --- /dev/null +++ b/modules/web-console/frontend/app/components/ignite-chart/controller.js @@ -0,0 +1,320 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import _ from 'lodash'; + +/** + * @typedef {{x: number, y: {[key: string]: number}}} IgniteChartDataPoint + */ + +const RANGE_RATE_PRESET = [{ + label: '1 min', + value: 1 +}, { + label: '5 min', + value: 5 +}, { + label: '10 min', + value: 10 +}, { + label: '15 min', + value: 15 +}, { + label: '30 min', + value: 30 +}]; + +export class IgniteChartController { + /** @type {import('chart.js').ChartConfiguration} */ + chartOptions; + /** @type {string} */ + chartTitle; + /** @type {IgniteChartDataPoint} */ + chartDataPoint; + /** @type {Array<IgniteChartDataPoint>} */ + chartHistory; + newPoints = []; + + static $inject = ['$element', 'IgniteChartColors', '$filter']; + + /** + * @param {JQLite} $element + * @param {ng.IScope} $scope + * @param {ng.IFilterService} $filter + */ + constructor($element, IgniteChartColors, $filter) { + this.$element = $element; + this.IgniteChartColors = IgniteChartColors; + + this.datePipe = $filter('date'); + this.ranges = RANGE_RATE_PRESET; + this.currentRange = this.ranges[0]; + this.maxRangeInMilliseconds = RANGE_RATE_PRESET[RANGE_RATE_PRESET.length - 1].value * 60 * 1000; + this.ctx = this.$element.find('canvas')[0].getContext('2d'); + + this.localHistory = []; + this.updateIsBusy = false; + } + + $onDestroy() { + if (this.chart) this.chart.destroy(); + this.$element = this.ctx = this.chart = null; + } + + $onInit() { + this.chartColors = _.get(this.chartOptions, 'chartColors', this.IgniteChartColors); + } + + /** + * @param {{chartOptions: ng.IChangesObject<import('chart.js').ChartConfiguration>, chartTitle: ng.IChangesObject<string>, chartDataPoint: ng.IChangesObject<IgniteChartDataPoint>, chartHistory: ng.IChangesObject<Array<IgniteChartDataPoint>>}} changes + */ + $onChanges(changes) { + if (this.chart && _.get(changes, 'refreshRate.currentValue')) + this.onRefreshRateChanged(_.get(changes, 'refreshRate.currentValue')); + + // TODO: Investigate other signaling for resetting component state. + if (changes.chartDataPoint && _.isNil(changes.chartDataPoint.currentValue)) { + this.clearDatasets(); + return; + } + + if (changes.chartHistory && changes.chartHistory.currentValue && changes.chartHistory.currentValue.length !== changes.chartHistory.previousValue.length) { + if (!this.chart) + this.initChart(); + + this.clearDatasets(); + this.localHistory = [...changes.chartHistory.currentValue]; + + this.newPoints.splice(0, this.newPoints.length, ...changes.chartHistory.currentValue); + + this.onRefresh(); + this.rerenderChart(); + return; + } + + if (this.chartDataPoint && changes.chartDataPoint) { + if (!this.chart) + this.initChart(); + + this.newPoints.push(this.chartDataPoint); + this.localHistory.push(this.chartDataPoint); + } + } + + async initChart() { + /** @type {import('chart.js').ChartConfiguration} */ + this.config = { + type: 'line', + data: { + datasets: [] + }, + options: { + elements: { + line: { + tension: 0 + }, + point: { + radius: 2, + pointStyle: 'rectRounded' + } + }, + animation: { + duration: 0 // general animation time + }, + hover: { + animationDuration: 0 // duration of animations when hovering an item + }, + responsiveAnimationDuration: 0, // animation duration after a resize + maintainAspectRatio: false, + responsive: true, + legend: { + display: false + }, + scales: { + xAxes: [{ + type: 'realtime', + display: true, + time: { + displayFormats: { + second: 'HH:mm:ss', + minute: 'HH:mm:ss', + hour: 'HH:mm:ss' + } + }, + ticks: { + maxRotation: 0, + minRotation: 0 + } + }], + yAxes: [{ + type: 'linear', + display: true, + ticks: { + min: 0, + beginAtZero: true, + maxTicksLimit: 4, + callback: (value, index, labels) => { + if (value === 0) + return 0; + + if (_.max(labels) <= 4000 && value <= 4000) + return value; + + if (_.max(labels) <= 1000000 && value <= 1000000) + return `${value / 1000}K`; + + if ((_.max(labels) <= 4000000 && value >= 500000) || (_.max(labels) > 4000000)) + return `${value / 1000000}M`; + + return value; + } + } + }] + }, + tooltips: { + mode: 'index', + position: 'nearest', + intersect: false, + xPadding: 20, + yPadding: 20, + bodyFontSize: 13, + callbacks: { + title: (tooltipItem) => { + return tooltipItem[0].xLabel.slice(0, -7); + }, + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + + return `${_.startCase(label)}: ${tooltipItem.yLabel} per sec`; + }, + labelColor: (tooltipItem) => { + return { + borderColor: 'rgba(255,255,255,0.5)', + borderWidth: 0, + boxShadow: 'none', + backgroundColor: this.chartColors[tooltipItem.datasetIndex] + }; + } + } + }, + plugins: { + streaming: { + duration: this.currentRange.value * 1000 * 60, + frameRate: 1000 / this.refreshRate || 1 / 3, + refresh: this.refreshRate || 3000, + onRefresh: () => { + this.onRefresh(); + } + } + } + } + }; + + this.config = _.merge(this.config, this.chartOptions); + + const chartModule = await import('chart.js'); + const Chart = chartModule.default; + + await import('chartjs-plugin-streaming'); + + this.chart = new Chart(this.ctx, this.config); + this.changeXRange(this.currentRange); + } + + onRefresh() { + this.newPoints.forEach((point) => { + this.appendChartPoint(point); + }); + + this.newPoints.splice(0, this.newPoints.length); + } + + /** + * @param {IgniteChartDataPoint} dataPoint + */ + appendChartPoint(dataPoint) { + Object.keys(dataPoint.y).forEach((key) => { + if (this.checkDatasetCanBeAdded(key)) { + let datasetIndex = this.findDatasetIndex(key); + + if (datasetIndex < 0) { + datasetIndex = this.config.data.datasets.length; + this.addDataset(key); + } + + // Prune excessive data points. + if (this.maxPointsNumber && this.config.data.datasets[datasetIndex].length - this.maxPointsNumber > 0) + this.config.data.datasets[datasetIndex].data.splice(0, this.config.data.datasets[datasetIndex].length - this.maxPointsNumber); + + this.config.data.datasets[datasetIndex].data.push({x: dataPoint.x, y: dataPoint.y[key]}); + this.config.data.datasets[datasetIndex].borderColor = this.chartOptions.chartColors[datasetIndex]; + this.config.data.datasets[datasetIndex].borderWidth = 2; + this.config.data.datasets[datasetIndex].fill = false; + } + }); + } + + /** + * Checks if a key of dataset can be added to chart or should be ignored. + * @param dataPointKey {String} + * @return {Boolean} + */ + checkDatasetCanBeAdded(dataPointKey) { + // If datasetLegendMapping is empty all keys are allowed. + if (!this.config.datasetLegendMapping) + return true; + + return Object.keys(this.config.datasetLegendMapping).includes(dataPointKey); + } + + clearDatasets() { + if (!_.isNil(this.config)) + this.config.data.datasets.forEach((dataset) => dataset.data = []); + } + + addDataset(datasetName) { + if (this.findDatasetIndex(datasetName) >= 0) + throw new Error(`Dataset with name ${datasetName} is already in chart`); + else + this.config.data.datasets.push({ label: datasetName, data: [] }); + } + + findDatasetIndex(searchedDatasetLabel) { + return this.config.data.datasets.findIndex((dataset) => dataset.label === searchedDatasetLabel); + } + + changeXRange(range) { + const deltaInMilliSeconds = range.value * 60 * 1000; + this.chart.config.options.plugins.streaming.duration = deltaInMilliSeconds; + + this.clearDatasets(); + this.newPoints.splice(0, this.newPoints.length, ...this.localHistory); + + this.onRefresh(); + this.rerenderChart(); + } + + onRefreshRateChanged(refreshRate) { + this.chart.config.options.plugins.streaming.frameRate = 1000 / refreshRate; + this.chart.config.options.plugins.streaming.refresh = refreshRate; + this.rerenderChart(); + } + + rerenderChart() { + this.chart.update(); + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/components/ignite-chart/index.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/ignite-chart/index.js b/modules/web-console/frontend/app/components/ignite-chart/index.js new file mode 100644 index 0000000..337ba36 --- /dev/null +++ b/modules/web-console/frontend/app/components/ignite-chart/index.js @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import angular from 'angular'; + +import { IgniteChartController } from './controller'; +import template from './template.pug'; +import './style.scss'; + +export default angular + .module('ignite-console.ignite-chart', []) + .component('igniteChart', { + controller: IgniteChartController, + template, + bindings: { + chartOptions: '<', + chartDataPoint: '<', + chartHistory: '<', + chartTitle: '<', + chartColors: '<', + maxPointsNumber: '<', + refreshRate: '<' + } + }); http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/components/ignite-chart/style.scss ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/ignite-chart/style.scss b/modules/web-console/frontend/app/components/ignite-chart/style.scss new file mode 100644 index 0000000..be0fb6d --- /dev/null +++ b/modules/web-console/frontend/app/components/ignite-chart/style.scss @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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. + */ + +ignite-chart { + display: flex; + flex-direction: column; + + height: 100%; + + header { + display: flex; + flex-direction: row; + justify-content: space-between; + box-sizing: content-box; + + height: 36px; + min-height: 36px; + padding: 7px 21px; + + border-bottom: 1px solid #d4d4d4; + + h5 { + margin: 0; + + font-size: 16px; + font-weight: normal; + line-height: 36px; + } + + > div { + display: flex; + align-items: center; + } + } + + .ignite-chart-placeholder { + display: block; + height: calc(100% - 71px); + margin-top: 20px; + } + + .no-data { + position: absolute; + top: 50%; + + width: 100%; + padding: 0 20px; + + border-radius: 0 0 4px 4px; + + font-style: italic; + line-height: 16px; + text-align: center; + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/components/ignite-chart/template.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/ignite-chart/template.pug b/modules/web-console/frontend/app/components/ignite-chart/template.pug new file mode 100644 index 0000000..076d8d4 --- /dev/null +++ b/modules/web-console/frontend/app/components/ignite-chart/template.pug @@ -0,0 +1,36 @@ +//- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF 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. + +header + div + h5 {{ $ctrl.chartTitle }} + ignite-chart-series-selector(chart-api='$ctrl.chart') + + div + span Range: + button.btn-ignite.btn-ignite--link-success.link-primary( + ng-model='$ctrl.currentRange' + ng-change='$ctrl.changeXRange($ctrl.currentRange)' + bs-options='item as item.label for item in $ctrl.ranges' + bs-select + ) + +.ignite-chart-placeholder + canvas + +.no-data(ng-if='!$ctrl.config.data.datasets') + | No Data #[br] + | Make sure you are connected to the right grid. http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/app/primitives/btn/index.scss ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/primitives/btn/index.scss b/modules/web-console/frontend/app/primitives/btn/index.scss index 2d9e9c4..277b1ae 100644 --- a/modules/web-console/frontend/app/primitives/btn/index.scss +++ b/modules/web-console/frontend/app/primitives/btn/index.scss @@ -319,10 +319,19 @@ $btn-content-padding-with-border: 9px 11px; $line-color: $ignite-brand-success; border-right-color: change-color($line-color, $saturation: 63%, $lightness: 33%); } + + &[disabled] .btn-ignite.btn-ignite--primary { + border-right-color: change-color($ignite-brand-primary, $lightness: 83%); + } + + &[disabled] .btn-ignite.btn-ignite--success { + border-right-color: change-color($ignite-brand-success, $lightness: 83%); + } } @mixin ignite-link($color, $color-hover) { color: $color; + text-decoration: none; &:hover, &.hover, &:focus, &.focus { http://git-wip-us.apache.org/repos/asf/ignite/blob/4d9f4504/modules/web-console/frontend/package.json ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/package.json b/modules/web-console/frontend/package.json index 02acbe0..6a6b97f 100644 --- a/modules/web-console/frontend/package.json +++ b/modules/web-console/frontend/package.json @@ -54,6 +54,8 @@ "brace": "0.10.0", "browser-update": "3.1.13", "bson-objectid": "1.1.5", + "chart.js": "2.7.2", + "chartjs-plugin-streaming": "1.5.0", "file-saver": "1.3.3", "font-awesome": "4.7.0", "jquery": "3.2.1",