636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU Affero General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU Affero General Public License for more details.
646 |
647 | You should have received a copy of the GNU Affero General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If your software can interact with users remotely through a computer
653 | network, you should also make sure that it provides a way for users to
654 | get its source. For example, if your program is a web application, its
655 | interface could display a "Source" link that leads users to an archive
656 | of the code. There are many ways you could offer source, and different
657 | solutions will be better for different programs; see section 13 for the
658 | specific requirements.
659 |
660 | You should also get your employer (if you work as a programmer) or school,
661 | if any, to sign a "copyright disclaimer" for the program, if necessary.
662 | For more information on this, and how to apply and follow the GNU AGPL, see
663 | .
664 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ECharts Widget for Zabbix - [Monzphere](https://monzphere.com)
2 |
3 | This module adds a customizable widget to Zabbix that allows creating interactive charts using the ECharts library.
4 |
5 | 
6 |
7 |
8 | ## 🚀 Features
9 |
10 | - Support for multiple chart types:
11 | - Gauge
12 | - Liquid Chart
13 | - Pie Chart
14 | - Horizontal Bar Chart
15 | - Multi-level Gauge
16 | - Treemap Chart
17 | - Nightingale Rose Chart
18 | - Funnel Chart
19 | - Treemap/Sunburst Chart
20 | - LLD Table
21 |
22 | - Simplified configuration through Zabbix interface
23 | - Automatic item unit detection
24 | - Real-time updates
25 | - Light/Dark theme support
26 | - Interactive and responsive tooltips
27 | - Interactive zoom and navigation
28 | - Automatic value formatting based on item units
29 |
30 | ## 📊 Chart Types
31 |
32 | ### Gauge
33 | - Displays value in a circular gauge format
34 | - Dynamic colors based on value
35 | - Support for multiple color ranges
36 | - Smooth value update animation
37 |
38 | ### Liquid Chart
39 | - Liquid fill visualization
40 | - Fluid animation
41 | - Dynamic colors based on value
42 | - Perfect for percentage representation
43 |
44 | ### Pie Chart
45 | - Pie/donut visualization
46 | - Support for multiple values
47 | - Informative labels
48 | - Hover interaction
49 |
50 | ### Horizontal Bar Chart
51 | - Horizontal bar visualization
52 | - Automatic value-based sorting
53 | - Pagination support for many items
54 | - Informative labels with units
55 |
56 | ### Multi-level Gauge
57 | - Multiple gauges in a single chart
58 | - Distinct colors for each level
59 | - Independent level animation
60 | - Ideal for comparisons
61 |
62 | ### Treemap Chart
63 | - Hierarchical data visualization
64 | - Interactive zoom
65 | - Dynamic value-based colors
66 | - Breadcrumb navigation
67 |
68 | ### Nightingale Rose Chart
69 | - Segmented circular visualization
70 | - Perfect for value comparison
71 | - Informative tooltips
72 | - Interactive animation
73 |
74 | ### Funnel Chart
75 | - Funnel-shaped visualization
76 | - Ideal for sequential processes
77 | - Informative labels
78 | - Smooth animation
79 |
80 | ### Treemap/Sunburst Chart
81 | - Automatic alternation between views
82 | - Animated transition
83 | - Rich interaction
84 | - Perfect for hierarchical data
85 |
86 | ### LLD Table
87 | - Table format visualization
88 | - Pagination support
89 | - Column sorting
90 | - Automatic value formatting
91 |
92 | ## 🔧 Configuration
93 |
94 | 1. Select desired chart type
95 | 2. Choose items to monitor
96 | 3. The widget automatically:
97 | - Detects item units
98 | - Formats values appropriately
99 | - Adjusts colors and scales
100 | - Configures tooltips and interactions
101 |
102 | ## 📈 Value Formatting
103 |
104 | The widget automatically formats values based on item units:
105 | - Bytes (B, KB, MB, GB, TB)
106 | - Percentages (%)
107 | - Rates per second (B/s, KB/s, etc)
108 | - Numeric values with appropriate precision
109 | - Scientific notation for very large/small values
110 |
111 | ## 🎨 Visual Customization
112 |
113 | - Adaptive colors based on values
114 | - Light/Dark themes
115 | - Responsive and always visible tooltips
116 | - Smooth interactions and animations
117 | - Responsive layout that adapts to widget size
118 |
119 | ## 🤝 Contributing
120 |
121 | Contributions are welcome! Please feel free to submit pull requests.
122 |
123 | ## 📄 License
124 |
125 | This project is licensed under the AGPL-3.0 license
126 |
--------------------------------------------------------------------------------
/Widget.php:
--------------------------------------------------------------------------------
1 | [
34 | 'No data' => _('No data'),
35 | 'Error updating chart' => _('Error updating chart')
36 | ]
37 | ];
38 | }
39 | }
--------------------------------------------------------------------------------
/actions/WidgetView.php:
--------------------------------------------------------------------------------
1 | $this->getInput('name', $this->widget->getName()),
38 | 'body' => '',
39 | 'items_data' => $items_data,
40 | 'items_meta' => $items_meta,
41 | 'fields_values' => $this->fields_values,
42 | 'display_type' => $this->fields_values['display_type'] ?? WidgetForm::DISPLAY_TYPE_GAUGE,
43 | 'user' => [
44 | 'debug_mode' => $this->getDebugMode()
45 | ]
46 | ];
47 |
48 | // Verificar se temos algum filtro de host ou grupo configurado
49 | $has_host_filter = false;
50 |
51 | // Dashboard de template com override_hostid
52 | if ($this->isTemplateDashboard() && !empty($this->fields_values['override_hostid'])) {
53 | $has_host_filter = true;
54 | }
55 | // Dashboard normal com hostids ou groupids
56 | else if (!empty($this->fields_values['hostids']) || !empty($this->fields_values['groupids'])) {
57 | $has_host_filter = true;
58 | }
59 |
60 | // Se não temos filtros, retornar dados vazios
61 | if (!$has_host_filter) {
62 | $this->setResponse(new CControllerResponseData($data));
63 | return;
64 | }
65 |
66 | // Continua apenas se tiver filtros configurados
67 | $options = [
68 | 'output' => ['itemid', 'value_type', 'name', 'units', 'lastvalue', 'lastclock', 'delay', 'history'],
69 | 'webitems' => true,
70 | 'preservekeys' => true,
71 | 'selectHosts' => ['name']
72 | ];
73 |
74 | // Verificar se estamos em um dashboard de template e se o override_hostid está definido
75 | if ($this->isTemplateDashboard() && !empty($this->fields_values['override_hostid'])) {
76 | // Em dashboard de template com override_hostid definido, usamos o host especificado
77 | $options['hostids'] = $this->fields_values['override_hostid'];
78 | }
79 | else {
80 | // Caso contrário, seguimos o fluxo normal
81 | if (!empty($this->fields_values['groupids'])) {
82 | $options['groupids'] = $this->fields_values['groupids'];
83 | }
84 |
85 | if (!empty($this->fields_values['hostids'])) {
86 | $options['hostids'] = $this->fields_values['hostids'];
87 | }
88 | }
89 |
90 | if (!empty($this->fields_values['items'])) {
91 | $patterns = [];
92 | foreach ($this->fields_values['items'] as $pattern) {
93 |
94 | $cleanPattern = preg_replace('/^\*:\s*/', '', $pattern);
95 | $patterns[] = $cleanPattern;
96 | }
97 |
98 | $options['search'] = ['name' => $patterns];
99 | $options['searchByAny'] = true;
100 | $options['searchWildcardsEnabled'] = true;
101 | }
102 |
103 |
104 | if (!empty($this->fields_values['host_tags'])) {
105 | $options['hostTags'] = $this->fields_values['host_tags'];
106 | $options['evaltype'] = $this->fields_values['evaltype_host'];
107 | }
108 |
109 |
110 | if (!empty($this->fields_values['item_tags'])) {
111 | $options['tags'] = $this->fields_values['item_tags'];
112 | $options['evaltype'] = $this->fields_values['evaltype_item'];
113 | }
114 |
115 | $db_items = API::Item()->get($options);
116 |
117 | if ($db_items) {
118 | foreach ($db_items as $itemid => $item) {
119 |
120 | $value = $item['lastvalue'];
121 |
122 | if ($value !== null && $value !== '') {
123 | $raw_value = preg_replace('/[^\d.-]/', '', $value);
124 | $items_data[$itemid] = $raw_value;
125 | }
126 | else {
127 | $items_data[$itemid] = '0';
128 | }
129 |
130 |
131 | $items_meta[$itemid] = [
132 | 'name' => $item['name'],
133 | 'host' => $item['hosts'][0]['name'],
134 | 'units' => $item['units'],
135 | 'value_type' => $item['value_type'],
136 | 'delay' => $item['delay'],
137 | 'history' => $item['history'],
138 | 'lastclock' => $item['lastclock']
139 | ];
140 | }
141 | }
142 |
143 | $columns = $this->fields_values['columns'] ?? [];
144 | foreach ($items_meta as $itemid => &$meta) {
145 | foreach ($columns as $column) {
146 | if (isset($column['item']) && $column['item'] == $itemid) {
147 | $meta['name'] = $column['name'] ?? $meta['name'];
148 | $meta['units'] = $column['units'] ?? $meta['units'];
149 | break;
150 | }
151 | }
152 | }
153 | unset($meta);
154 |
155 | // Atualizar os dados com os resultados da consulta
156 | $data['items_data'] = $items_data;
157 | $data['items_meta'] = $items_meta;
158 |
159 | $this->setResponse(new CControllerResponseData($data));
160 | }
161 | }
--------------------------------------------------------------------------------
/assets/css/widget.css:
--------------------------------------------------------------------------------
1 | /* Estilos base para o widget */
2 | div.dashboard-widget-echarts {
3 | display: grid;
4 | grid-template-rows: 1fr;
5 | padding: 0;
6 | }
7 |
8 | div.dashboard-widget-echarts .chart {
9 | display: grid;
10 | align-items: center;
11 | justify-items: center;
12 | }
13 |
14 | /* Tema Azul */
15 | body.blue-theme div.dashboard-widget-echarts .chart text,
16 | body.blue-theme div.dashboard-widget-echarts .chart .echarts-tooltip,
17 | body.blue-theme div.dashboard-widget-echarts .chart .echarts-legend,
18 | body.blue-theme div.dashboard-widget-echarts .chart .gauge-title,
19 | body.blue-theme div.dashboard-widget-echarts .chart .gauge-detail {
20 | fill: #000000 !important;
21 | color: #000000 !important;
22 | -webkit-text-fill-color: #000000 !important;
23 | }
24 |
25 | /* Tema Escuro */
26 | body.dark-theme div.dashboard-widget-echarts .chart text,
27 | body.dark-theme div.dashboard-widget-echarts .chart .echarts-tooltip,
28 | body.dark-theme div.dashboard-widget-echarts .chart .echarts-legend,
29 | body.dark-theme div.dashboard-widget-echarts .chart .gauge-title,
30 | body.dark-theme div.dashboard-widget-echarts .chart .gauge-detail {
31 | fill: #ffffff !important;
32 | color: #ffffff !important;
33 | -webkit-text-fill-color: #ffffff !important;
34 | }
35 |
36 | /* Estilos específicos para cada tipo de gráfico */
37 | div.dashboard-widget-echarts .chart canvas {
38 | background: transparent;
39 | }
40 |
41 | div.dashboard-widget-echarts .description {
42 | padding-bottom: 8px;
43 | font-size: 1.750em;
44 | line-height: 1.2;
45 | text-align: center;
46 | }
47 |
48 | .dashboard-grid-widget-hidden-header div.dashboard-widget-echarts .chart {
49 | margin-top: 8px;
50 | }
51 |
52 | /* Tema escuro - cores padrão */
53 | .echarts-widget {
54 | color: #ffffff;
55 | }
56 |
57 | .echarts-widget .echarts {
58 | color: #ffffff;
59 | }
60 |
61 | /* Tema azul */
62 | .blue-theme .echarts-widget {
63 | color: #000000;
64 | }
65 |
66 | .blue-theme .echarts-widget .echarts {
67 | color: #000000;
68 | }
69 |
70 | /* Estilos específicos para elementos do gráfico */
71 | .echarts-widget .echarts-title,
72 | .echarts-widget .echarts-label,
73 | .echarts-widget .echarts-axis-label,
74 | .echarts-widget .gauge-title,
75 | .echarts-widget .gauge-detail {
76 | color: inherit !important;
77 | fill: inherit !important;
78 | -webkit-text-fill-color: inherit !important;
79 | }
80 |
81 | /* Tema escuro - cores específicas */
82 | .echarts-widget .gauge-current {
83 | color: #5470c6;
84 | }
85 |
86 | .echarts-widget .gauge-desired {
87 | color: #91cc75;
88 | }
89 |
90 | .echarts-widget .gauge-undesired {
91 | color: #fac858;
92 | }
93 |
94 | /* Tema azul - cores específicas */
95 | .blue-theme .echarts-widget .gauge-current {
96 | color: #5470c6;
97 | }
98 |
99 | .blue-theme .echarts-widget .gauge-desired {
100 | color: #91cc75;
101 | }
102 |
103 | .blue-theme .echarts-widget .gauge-undesired {
104 | color: #fac858;
105 | }
106 |
107 | .theme-blue .widget-echarts .chart {
108 | color: #000000;
109 | }
110 |
111 | .theme-blue .widget-echarts .chart .title {
112 | color: #000000 !important;
113 | }
114 |
115 | .theme-blue .widget-echarts .chart .detail {
116 | color: #000000 !important;
117 | }
118 |
119 | .theme-dark .widget-echarts .chart {
120 | color: #ffffff;
121 | }
122 |
123 | .theme-dark .widget-echarts .chart .title {
124 | color: #ffffff !important;
125 | }
126 |
127 | .theme-dark .widget-echarts .chart .detail {
128 | color: #ffffff !important;
129 | }
130 |
131 | /* Estilos específicos para o multi-gauge */
132 | .theme-blue .widget-echarts .chart .gauge-title {
133 | color: #000000 !important;
134 | }
135 |
136 | .theme-blue .widget-echarts .chart .gauge-detail {
137 | color: #000000 !important;
138 | }
139 |
140 | .theme-dark .widget-echarts .chart .gauge-title {
141 | color: #ffffff !important;
142 | }
143 |
144 | .theme-dark .widget-echarts .chart .gauge-detail {
145 | color: #ffffff !important;
146 | }
147 |
148 | /* Força a cor do texto no tema azul */
149 | .blue-theme .echarts-widget text,
150 | .blue-theme .echarts-widget .gauge-title text,
151 | .blue-theme .echarts-widget .gauge-detail text {
152 | fill: #000000 !important;
153 | color: #000000 !important;
154 | text-fill-color: #000000 !important;
155 | -webkit-text-fill-color: #000000 !important;
156 | }
157 |
158 | /* Força a cor do texto no tema escuro */
159 | .dark-theme .echarts-widget text,
160 | .dark-theme .echarts-widget .gauge-title text,
161 | .dark-theme .echarts-widget .gauge-detail text {
162 | fill: #ffffff !important;
163 | color: #ffffff !important;
164 | -webkit-text-fill-color: #ffffff !important;
165 | }
--------------------------------------------------------------------------------
/assets/js/class.widget.js:
--------------------------------------------------------------------------------
1 | class WidgetEcharts extends CWidget {
2 |
3 | static CONFIG_TYPE_JSON = 0;
4 | static CONFIG_TYPE_JAVASCRIPT = 1;
5 |
6 | // Constants for unit types
7 | static UNIT_TYPE_NONE = 0;
8 | static UNIT_TYPE_PERCENTAGE = 1;
9 | static UNIT_TYPE_BITS = 2;
10 |
11 | // Constants for chart types
12 | static DISPLAY_TYPE_GAUGE = 0;
13 | static DISPLAY_TYPE_LIQUID = 1;
14 | static DISPLAY_TYPE_PIE = 2;
15 | static DISPLAY_TYPE_HBAR = 3;
16 | static DISPLAY_TYPE_MULTI_GAUGE = 4;
17 | static DISPLAY_TYPE_TREEMAP = 5;
18 | static DISPLAY_TYPE_ROSE = 6;
19 | static DISPLAY_TYPE_FUNNEL = 8;
20 | static DISPLAY_TYPE_TREEMAP_SUNBURST = 9;
21 | static DISPLAY_TYPE_LLD_TABLE = 10;
22 |
23 | // Constantes para temas de cores
24 | static COLOR_THEME_DEFAULT = 0;
25 | static COLOR_THEME_ZABBIX = 1;
26 | static COLOR_THEME_PASTEL = 2;
27 | static COLOR_THEME_BRIGHT = 3;
28 | static COLOR_THEME_DARK = 4;
29 | static COLOR_THEME_BLUE = 5;
30 |
31 | // Constants for trigger severity
32 | static TRIGGER_SEVERITY_COLORS = {
33 | 0: '#97AAB3', // Not classified
34 | 1: '#7499FF', // Information
35 | 2: '#FFC859', // Warning
36 | 3: '#FFA059', // Average
37 | 4: '#E97659', // High
38 | 5: '#E45959' // Disaster
39 | };
40 |
41 | // Temas de cores disponíveis
42 | static COLOR_THEMES = {
43 | // Tema padrão
44 | [WidgetEcharts.COLOR_THEME_DEFAULT]: [
45 | '#5470c6', // Azul
46 | '#91cc75', // Verde
47 | '#fac858', // Amarelo
48 | '#ee6666', // Vermelho
49 | '#73c0de', // Azul claro
50 | '#3ba272', // Verde escuro
51 | '#fc8452', // Laranja
52 | '#9a60b4', // Roxo
53 | '#ea7ccc', // Rosa
54 | '#c23531' // Vermelho escuro
55 | ],
56 | // Tema cores do Zabbix
57 | [WidgetEcharts.COLOR_THEME_ZABBIX]: [
58 | '#7499FF', // Azul Informativo
59 | '#FFC859', // Amarelo Aviso
60 | '#FFA059', // Laranja Médio
61 | '#E97659', // Laranja-vermelho Alto
62 | '#E45959', // Vermelho Desastre
63 | '#97AAB3', // Cinza Não classificado
64 | '#009900', // Verde
65 | '#0000EE', // Azul escuro
66 | '#AA00BB', // Roxo
67 | '#FF5500' // Laranja vibrante
68 | ],
69 | // Tema de cores pastel
70 | [WidgetEcharts.COLOR_THEME_PASTEL]: [
71 | '#B5D8EB', // Azul pastel
72 | '#C4E0B2', // Verde pastel
73 | '#FFE699', // Amarelo pastel
74 | '#FFCCCC', // Vermelho pastel
75 | '#D4BBDD', // Roxo pastel
76 | '#F2C1D5', // Rosa pastel
77 | '#FFCBA4', // Laranja pastel
78 | '#B5EAD7', // Menta pastel
79 | '#E2F0CB', // Verde-limão pastel
80 | '#FDCFDF' // Magenta pastel
81 | ],
82 | // Tema de cores brilhantes
83 | [WidgetEcharts.COLOR_THEME_BRIGHT]: [
84 | '#0088FF', // Azul brilhante
85 | '#00CC66', // Verde brilhante
86 | '#FFCC00', // Amarelo brilhante
87 | '#FF3333', // Vermelho brilhante
88 | '#CC33FF', // Roxo brilhante
89 | '#FF66CC', // Rosa brilhante
90 | '#FF8800', // Laranja brilhante
91 | '#00FFFF', // Ciano brilhante
92 | '#66FF33', // Lima brilhante
93 | '#FF33FF' // Magenta brilhante
94 | ],
95 | // Tema de cores escuras
96 | [WidgetEcharts.COLOR_THEME_DARK]: [
97 | '#003366', // Azul escuro
98 | '#006633', // Verde escuro
99 | '#996633', // Marrom escuro
100 | '#993333', // Vermelho escuro
101 | '#660066', // Roxo escuro
102 | '#990033', // Borgonha escuro
103 | '#663300', // Marrom-laranja escuro
104 | '#006666', // Verde-azulado escuro
105 | '#333300', // Verde oliva escuro
106 | '#330033' // Púrpura escuro
107 | ],
108 | // Tema monocromático (tons de azul)
109 | [WidgetEcharts.COLOR_THEME_BLUE]: [
110 | '#0A2463', // Azul mais escuro
111 | '#1E3888', // Azul escuro
112 | '#3066BE', // Azul médio-escuro
113 | '#5F9DF7', // Azul médio
114 | '#8AB6F9', // Azul médio-claro
115 | '#B6D0FA', // Azul claro
116 | '#DAE7FB', // Azul muito claro
117 | '#256EFF', // Azul brilhante
118 | '#478FFF', // Azul-ciano
119 | '#30BCED' // Ciano
120 | ]
121 | };
122 |
123 | onInitialize() {
124 | super.onInitialize();
125 |
126 | this._refresh_frame = null;
127 | this._chart_container = null;
128 | this._chart = null;
129 | this._items_data = {};
130 | this._items_meta = {};
131 | this._fields_values = {
132 | display_type: 0,
133 | unit_type: 0,
134 | echarts_config: null,
135 | config_type: 0,
136 | color_theme: 0
137 | };
138 | }
139 |
140 | /**
141 | * Processa a resposta da atualização do widget
142 | * @param {Object} response - A resposta da API com os dados do widget
143 | */
144 | processUpdateResponse(response) {
145 | // Update internal data
146 | this._items_data = response.items_data || {};
147 | this._items_meta = response.items_meta || {};
148 | this._fields_values = response.fields_values || this._fields_values;
149 |
150 | // Processar valores em notação científica nos dados de resposta
151 | if (this._items_data) {
152 | Object.keys(this._items_data).forEach(itemId => {
153 | const value = this._items_data[itemId];
154 |
155 | // Verificar se o valor está em notação científica
156 | if (typeof value === 'string') {
157 | // Tratar o caso "1.83e+8 bps"
158 | const scientificWithUnitsMatch = value.match(/^(\d+\.\d+e[+-]\d+)\s*(\w+)$/i);
159 | if (scientificWithUnitsMatch) {
160 | const numValue = Number(scientificWithUnitsMatch[1]);
161 | const unitValue = scientificWithUnitsMatch[2];
162 |
163 | if (!isNaN(numValue)) {
164 | // Atualizar o valor para seu equivalente numérico
165 | this._items_data[itemId] = numValue;
166 |
167 | // Atualizar/adicionar a unidade aos metadados
168 | if (this._items_meta[itemId]) {
169 | this._items_meta[itemId].units = unitValue;
170 | this._items_meta[itemId].originalValue = value;
171 | }
172 | }
173 | }
174 | // Tratar valor em notação científica simples
175 | else if (value.match(/^\d+\.\d+e[+-]\d+$/i)) {
176 | const numValue = Number(value);
177 |
178 | if (!isNaN(numValue)) {
179 |
180 | // Atualizar o valor para seu equivalente numérico
181 | this._items_data[itemId] = numValue;
182 |
183 | // Armazenar o valor original nos metadados
184 | if (this._items_meta[itemId]) {
185 | this._items_meta[itemId].originalValue = value;
186 | }
187 | }
188 | }
189 | }
190 | });
191 | }
192 |
193 | // Prepare fields for context
194 | const fields = [];
195 | for (const [itemid, value] of Object.entries(this._items_data)) {
196 | const numericValue = parseFloat(value);
197 |
198 | if (isNaN(numericValue)) {
199 | console.warn(`Invalid value for item ${itemid}:`, value);
200 | continue;
201 | }
202 |
203 | // Get item metadata
204 | const meta = this._items_meta[itemid];
205 | if (!meta) continue;
206 |
207 | const field = {
208 | id: itemid,
209 | name: meta.name || `Item ${itemid}`,
210 | value: numericValue,
211 | values: [numericValue],
212 | units: meta.units || '',
213 | host: meta.host || 'Unknown host',
214 | value_type: meta.value_type || '0',
215 | delay: meta.delay || '0',
216 | history: meta.history || '7d',
217 | lastclock: meta.lastclock || ''
218 | };
219 |
220 | fields.push(field);
221 | }
222 |
223 | // Update context
224 | this._context = {
225 | panel: {
226 | data: {
227 | series: [{
228 | fields: fields
229 | }]
230 | },
231 | chart: null
232 | },
233 | zabbix: {
234 | items: {...this._items_data},
235 | items_meta: {...this._items_meta}
236 | },
237 | helpers: {
238 | formatDate: (timestamp) => {
239 | return new Date(timestamp * 1000).toLocaleString();
240 | },
241 | formatNumber: (value, decimals = 2) => {
242 | return Number(value).toFixed(decimals);
243 | },
244 | formatBytes: (bytes) => {
245 | const units = ['B', 'KB', 'MB', 'GB', 'TB'];
246 | let value = Math.abs(bytes);
247 | let unitIndex = 0;
248 |
249 | while (value >= 1024 && unitIndex < units.length - 1) {
250 | value /= 1024;
251 | unitIndex++;
252 | }
253 |
254 | return value.toFixed(2) + ' ' + units[unitIndex];
255 | },
256 | generateColors: (count) => {
257 | return Array(count).fill(0).map((_, i) => this._getColorByIndex(i));
258 | }
259 | }
260 | };
261 |
262 | // Set contents
263 | this.setContents(response);
264 | }
265 |
266 | setContents(response) {
267 | if (this._chart === null) {
268 | super.setContents(response);
269 |
270 | this._chart_container = this._body.querySelector('.chart');
271 | if (!this._chart_container) {
272 | console.error('Chart container not found');
273 | return;
274 | }
275 |
276 | // Ajusta o estilo do container para ocupar todo o espaço disponível sem scroll
277 | this._chart_container.style.cssText = `
278 | position: absolute;
279 | top: 0;
280 | left: 0;
281 | right: 0;
282 | bottom: 0;
283 | overflow: visible;
284 | `;
285 |
286 | try {
287 | // Initialize with dark theme and automatic renderer
288 | this._chart = echarts.init(this._chart_container, 'dark', {
289 | renderer: 'canvas',
290 | useDirtyRect: true
291 | });
292 |
293 | // Base configuration
294 | const baseOptions = {
295 | backgroundColor: 'transparent',
296 | textStyle: {
297 | color: '#fff'
298 | }
299 | };
300 |
301 | this._chart.setOption(baseOptions);
302 |
303 | // Register events
304 | this._chart.on('click', (params) => {
305 | console.log('Click on graph:', params);
306 | });
307 |
308 | this._resizeChart();
309 | }
310 | catch (error) {
311 | console.error('Error initializing chart:', error);
312 | return;
313 | }
314 | }
315 |
316 | this._updateChart();
317 | }
318 |
319 | _updateChart() {
320 | try {
321 | if (!this._chart || !this._context || !this._context.panel || !this._context.panel.data) {
322 | console.error('Context or chart not initialized correctly');
323 | return;
324 | }
325 |
326 | const data = this._context.panel.data.series[0];
327 | if (!data || !data.fields) {
328 | console.error('Data not available in expected format');
329 | return;
330 | }
331 |
332 | let options;
333 | const displayType = parseInt(this._fields_values.display_type);
334 |
335 | // Configuração base do tooltip que será mesclada com as configurações específicas
336 | const baseTooltipConfig = {
337 | confine: false,
338 | enterable: true,
339 | appendToBody: true,
340 | position: function (point, params, dom, rect, size) {
341 | // Obtém as dimensões e posição do widget
342 | const widgetEl = document.querySelector('.dashboard-grid-widget-container');
343 | const widgetRect = widgetEl ? widgetEl.getBoundingClientRect() : null;
344 |
345 | // Calcula a posição ideal do tooltip
346 | const viewWidth = document.documentElement.clientWidth;
347 | const viewHeight = document.documentElement.clientHeight;
348 | const tooltipWidth = size.contentSize[0];
349 | const tooltipHeight = size.contentSize[1];
350 |
351 | // Posição inicial (à direita do ponto)
352 | let x = point[0] + 15;
353 | let y = point[1];
354 |
355 | // Ajusta horizontalmente se necessário
356 | if (x + tooltipWidth > viewWidth) {
357 | x = point[0] - tooltipWidth - 15;
358 | }
359 |
360 | // Ajusta verticalmente se necessário
361 | if (y + tooltipHeight > viewHeight) {
362 | y = Math.max(0, viewHeight - tooltipHeight);
363 | }
364 |
365 | return [x, y];
366 | },
367 | backgroundColor: 'rgba(50, 50, 50, 0.9)',
368 | borderColor: 'rgba(255, 255, 255, 0.3)',
369 | borderWidth: 1,
370 | padding: [10, 15],
371 | textStyle: {
372 | color: '#fff',
373 | fontSize: 12
374 | },
375 | extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); border-radius: 4px; z-index: 1000000 !important; pointer-events: all !important;'
376 | };
377 |
378 | switch (displayType) {
379 | case WidgetEcharts.DISPLAY_TYPE_MULTI_GAUGE:
380 | options = this._createMultiGaugeChart(data);
381 | break;
382 | case WidgetEcharts.DISPLAY_TYPE_HBAR:
383 | options = this._createHorizontalBarChart(data);
384 | break;
385 | case WidgetEcharts.DISPLAY_TYPE_GAUGE:
386 | options = this._createGaugeChart(data);
387 | break;
388 | case WidgetEcharts.DISPLAY_TYPE_LIQUID:
389 | options = this._createLiquidChart(data);
390 | break;
391 | case WidgetEcharts.DISPLAY_TYPE_PIE:
392 | options = this._createPieChart(data);
393 | break;
394 | case WidgetEcharts.DISPLAY_TYPE_TREEMAP:
395 | options = this._createTreemapChart(data);
396 | break;
397 | case WidgetEcharts.DISPLAY_TYPE_ROSE:
398 | options = this._createRoseChart(data);
399 | break;
400 | case WidgetEcharts.DISPLAY_TYPE_FUNNEL:
401 | options = this._createFunnelChart(data);
402 | break;
403 | case WidgetEcharts.DISPLAY_TYPE_TREEMAP_SUNBURST:
404 | options = this._createTreemapSunburstChart(data);
405 | break;
406 | case WidgetEcharts.DISPLAY_TYPE_LLD_TABLE:
407 | options = this._createLLDTableChart(data);
408 | break;
409 | default:
410 | console.error('Unsupported chart type:', displayType);
411 | return;
412 | }
413 |
414 | if (!options) {
415 | console.error('Chart options not generated correctly');
416 | return;
417 | }
418 |
419 | // Mescla a configuração base do tooltip com as opções específicas do gráfico
420 | if (options.tooltip) {
421 | options.tooltip = { ...baseTooltipConfig, ...options.tooltip };
422 | } else {
423 | options.tooltip = baseTooltipConfig;
424 | }
425 |
426 | // Apply base options common to all charts
427 | const baseOptions = {
428 | backgroundColor: 'transparent'
429 | };
430 |
431 | // Merge base options with specific chart options
432 | const finalOptions = {...baseOptions, ...options};
433 |
434 | // Limpa eventos e handlers antigos antes de atualizar
435 | this._chart.off();
436 |
437 | // Atualiza o gráfico com as novas opções
438 | this._chart.setOption(finalOptions, true);
439 |
440 | // Reregistra os eventos necessários
441 | this._chart.on('click', (params) => {
442 | console.log('Click on graph:', params);
443 | });
444 | }
445 | catch (error) {
446 | console.error('Error updating chart:', error);
447 | }
448 | }
449 |
450 | _createMultiGaugeChart(data) {
451 | const fields = data.fields;
452 | if (!fields || !fields.length) return null;
453 |
454 | const value = parseFloat(fields[0].value);
455 | if (isNaN(value)) return null;
456 |
457 | const atual = value;
458 | const desejado = value <= 70 ? Math.max(0, value - atual) : 30;
459 | const naodesejado = Math.max(0, 100 - atual - desejado);
460 |
461 | const textStyle = {
462 | fontSize: 14,
463 | fontWeight: 'normal'
464 | };
465 |
466 | const gaugeData = [
467 | {
468 | value: atual,
469 | name: 'Current',
470 | title: {
471 | ...textStyle,
472 | offsetCenter: ['0%', '-40%'],
473 | color: '#5470c6'
474 | },
475 | detail: {
476 | ...textStyle,
477 | valueAnimation: true,
478 | offsetCenter: ['0%', '-25%'],
479 | formatter: function(value) {
480 | return value.toFixed(2) + '%';
481 | },
482 | backgroundColor: 'transparent',
483 | borderRadius: 10,
484 | padding: [5, 10],
485 | color: '#5470c6'
486 | },
487 | itemStyle: {
488 | color: '#5470c6'
489 | }
490 | },
491 | {
492 | value: desejado,
493 | name: 'Desired',
494 | title: {
495 | ...textStyle,
496 | offsetCenter: ['0%', '0%'],
497 | color: '#91cc75'
498 | },
499 | detail: {
500 | ...textStyle,
501 | valueAnimation: true,
502 | offsetCenter: ['0%', '15%'],
503 | formatter: function(value) {
504 | return value.toFixed(2) + '%';
505 | },
506 | backgroundColor: 'transparent',
507 | borderRadius: 10,
508 | padding: [5, 10],
509 | color: '#91cc75'
510 | },
511 | itemStyle: {
512 | color: '#91cc75'
513 | }
514 | },
515 | {
516 | value: naodesejado,
517 | name: 'Undesired',
518 | title: {
519 | ...textStyle,
520 | offsetCenter: ['0%', '40%'],
521 | color: '#fac858'
522 | },
523 | detail: {
524 | ...textStyle,
525 | valueAnimation: true,
526 | offsetCenter: ['0%', '55%'],
527 | formatter: function(value) {
528 | return value.toFixed(2) + '%';
529 | },
530 | backgroundColor: 'transparent',
531 | borderRadius: 10,
532 | padding: [5, 10],
533 | color: '#fac858'
534 | },
535 | itemStyle: {
536 | color: '#fac858'
537 | }
538 | }
539 | ];
540 |
541 | return {
542 | series: [{
543 | type: 'gauge',
544 | startAngle: 90,
545 | endAngle: -270,
546 | center: ['50%', '50%'],
547 | radius: '80%',
548 | pointer: {
549 | show: false
550 | },
551 | progress: {
552 | show: true,
553 | overlap: false,
554 | roundCap: true,
555 | clip: false,
556 | itemStyle: {
557 | borderWidth: 0
558 | }
559 | },
560 | axisLine: {
561 | lineStyle: {
562 | width: 20,
563 | color: [[1, 'rgba(255,255,255,0.1)']]
564 | }
565 | },
566 | splitLine: {
567 | show: false
568 | },
569 | axisTick: {
570 | show: false
571 | },
572 | axisLabel: {
573 | show: false
574 | },
575 | data: gaugeData,
576 | title: {
577 | ...textStyle
578 | },
579 | detail: {
580 | ...textStyle,
581 | width: 80,
582 | height: 20,
583 | borderWidth: 0
584 | }
585 | }]
586 | };
587 | }
588 |
589 | _getColorByIndex(index) {
590 | // Obter o tema de cores selecionado ou usar o padrão se não estiver definido
591 | const themeType = parseInt(this._fields_values.color_theme || WidgetEcharts.COLOR_THEME_DEFAULT);
592 |
593 | // Obter a paleta do tema selecionado ou voltar para o padrão se o tema não existir
594 | const theme = WidgetEcharts.COLOR_THEMES[themeType] ||
595 | WidgetEcharts.COLOR_THEMES[WidgetEcharts.COLOR_THEME_DEFAULT];
596 |
597 | // Retornar a cor baseada no índice (com rotação para índices maiores que o tamanho da paleta)
598 | return theme[index % theme.length];
599 | }
600 |
601 | _getColorByValue(value, min, max) {
602 | // Define thresholds for color ranges
603 | const thresholds = [
604 | { value: 20, color: '#91cc75' }, // Verde para valores baixos
605 | { value: 40, color: '#5470c6' }, // Azul para valores médio-baixos
606 | { value: 60, color: '#fac858' }, // Amarelo para valores médios
607 | { value: 80, color: '#fc8452' }, // Laranja para valores médio-altos
608 | { value: 100, color: '#ee6666' } // Vermelho para valores altos
609 | ];
610 |
611 | const percentage = ((value - min) / (max - min)) * 100;
612 |
613 | for (let i = 0; i < thresholds.length; i++) {
614 | if (percentage <= thresholds[i].value) {
615 | return thresholds[i].color;
616 | }
617 | }
618 | return thresholds[thresholds.length - 1].color;
619 | }
620 |
621 | _formatValueWithUnits(value, units) {
622 | try {
623 | if (value === null || value === undefined) {
624 | return 'N/A';
625 | }
626 |
627 | // Tratamento direto para valor em notação científica + unidade
628 | if (typeof value === 'string') {
629 | // Verificar padrão como "1.83e+8 bps"
630 | const scientificWithUnitsMatch = value.match(/^(\d+\.\d+e[+-]\d+)\s*(\w+)$/i);
631 | if (scientificWithUnitsMatch) {
632 | const numValue = Number(scientificWithUnitsMatch[1]);
633 | const unitValue = scientificWithUnitsMatch[2].toLowerCase();
634 |
635 | if (!isNaN(numValue)) {
636 | // Se a unidade for bps, usar formatação de bits
637 | if (unitValue === 'bps') {
638 | return this._formatBitsValue(numValue);
639 | }
640 | // Outras unidades científicas também podem ser processadas aqui
641 | }
642 | }
643 |
644 | // Verificar se é apenas notação científica sem unidade
645 | if (value.match(/^\d+\.\d+e[+-]\d+$/i)) {
646 | const numValue = Number(value);
647 | if (!isNaN(numValue)) {
648 | // Se temos unidades separadas, usamos elas para formatar
649 | if (units) {
650 | if (/bps/i.test(units)) {
651 | return this._formatBitsValue(numValue);
652 | }
653 | }
654 | // Sem unidades específicas, apenas formatamos o número
655 | return this._formatNumberBySize(numValue);
656 | }
657 | }
658 | }
659 |
660 | // Processar valor numérico normal
661 | const numValue = parseFloat(value);
662 | if (isNaN(numValue)) {
663 | return String(value);
664 | }
665 |
666 | // Se não houver unidades definidas, retorna apenas o valor formatado
667 | if (!units) {
668 | return this._formatNumberBySize(numValue);
669 | }
670 |
671 | // Processar com base nas unidades
672 | const normalizedUnits = units.toLowerCase().trim();
673 |
674 | // Caso especial para unidades bps (bits por segundo)
675 | if (/bps$/i.test(normalizedUnits)) {
676 | return this._formatBitsValue(numValue);
677 | }
678 |
679 | // Se a unidade for B, KB, MB, GB, TB ou variações, formata como bytes
680 | if (/^[kmgt]?b$/i.test(normalizedUnits.replace(/\s+/g, ''))) {
681 | return this._formatBytesValue(numValue);
682 | }
683 |
684 | // Se a unidade terminar com /s, formata com as unidades apropriadas
685 | if (normalizedUnits.endsWith('/s')) {
686 | return this._formatRateValue(numValue);
687 | }
688 |
689 | // Se a unidade for %, mantém como percentual
690 | if (normalizedUnits === '%' || normalizedUnits.includes('percent')) {
691 | return this._formatPercentValue(numValue);
692 | }
693 |
694 | // Para valores simples sem conversão especial
695 | return this._formatNumberBySize(numValue) + (units ? ' ' + units : '');
696 |
697 | } catch (error) {
698 | console.error('Error formatting value:', error, value, units);
699 | return String(value) + (units ? ' ' + units : '');
700 | }
701 | }
702 |
703 | // Adicionar um método específico para a tabela LLD
704 | _formatLLDTableValue(value, units) {
705 | // Para valores em notação científica (ex: 1.83e+8 bps)
706 | if (typeof value === 'string' && value.match(/^\d+\.\d+e[+-]\d+\s+bps$/i)) {
707 | const numericPart = value.split(' ')[0];
708 | const numericValue = Number(numericPart);
709 | if (!isNaN(numericValue)) {
710 | return this._formatBitsValue(numericValue);
711 | }
712 | }
713 |
714 | // Outros casos, usar o formatador geral
715 | return this._formatValueWithUnits(value, units);
716 | }
717 |
718 | _formatNumberBySize(value) {
719 | const absValue = Math.abs(value);
720 |
721 | if (absValue === 0) {
722 | return '0';
723 | } else if (absValue >= 1000000000) {
724 | return (value / 1000000000).toFixed(2) + ' G';
725 | } else if (absValue >= 1000000) {
726 | return (value / 1000000).toFixed(2) + ' M';
727 | } else if (absValue >= 1000) {
728 | return (value / 1000).toFixed(2) + ' K';
729 | } else if (absValue < 0.01 && absValue > 0) {
730 | return value.toExponential(2);
731 | } else {
732 | return value.toFixed(2);
733 | }
734 | }
735 |
736 | _formatBitsValue(value) {
737 | // Garantir que estamos trabalhando com um número
738 | value = Number(value);
739 | const absValue = Math.abs(value);
740 |
741 | if (absValue >= 1000000000) {
742 | return (value / 1000000000).toFixed(2) + ' Gbps';
743 | } else if (absValue >= 1000000) {
744 | return (value / 1000000).toFixed(2) + ' Mbps';
745 | } else if (absValue >= 1000) {
746 | return (value / 1000).toFixed(2) + ' Kbps';
747 | } else {
748 | return value.toFixed(2) + ' bps';
749 | }
750 | }
751 |
752 | _formatBytesValue(value) {
753 | const absValue = Math.abs(value);
754 |
755 | if (absValue >= 1099511627776) { // 1024^4
756 | return (value / 1099511627776).toFixed(2) + ' TB';
757 | } else if (absValue >= 1073741824) { // 1024^3
758 | return (value / 1073741824).toFixed(2) + ' GB';
759 | } else if (absValue >= 1048576) { // 1024^2
760 | return (value / 1048576).toFixed(2) + ' MB';
761 | } else if (absValue >= 1024) {
762 | return (value / 1024).toFixed(2) + ' KB';
763 | } else {
764 | return value.toFixed(2) + ' B';
765 | }
766 | }
767 |
768 | _formatRateValue(value) {
769 | // Simplificar para usar a formatação de bits
770 | return this._formatBitsValue(value);
771 | }
772 |
773 | _formatPercentValue(value) {
774 | return value.toFixed(2) + '%';
775 | }
776 |
777 | _createHorizontalBarChart(data) {
778 | if (typeof this._currentPage === 'undefined') {
779 | this._currentPage = 1;
780 | }
781 |
782 | let chartData = data.fields
783 | .map(field => ({
784 | name: field.name.replace('Container /', ''),
785 | value: parseFloat(field.value),
786 | units: field.units || ''
787 | }))
788 | .filter(item => !isNaN(item.value))
789 | .sort((a, b) => b.value - a.value);
790 |
791 | const itemsPerPage = 10;
792 | let displayData = chartData.slice(0, itemsPerPage).reverse();
793 |
794 | const chartOptions = {
795 | grid: {
796 | left: '15%',
797 | right: '5%',
798 | bottom: chartData.length > itemsPerPage ? '30px' : '10px',
799 | top: '0',
800 | containLabel: false
801 | },
802 | tooltip: {
803 | trigger: 'item',
804 | formatter: params => `${params.name}: ${this._formatValueWithUnits(params.value, params.data.units)}`
805 | },
806 | xAxis: {
807 | type: 'value',
808 | axisLabel: { show: false },
809 | axisTick: { show: false },
810 | axisLine: { show: false },
811 | splitLine: { show: false }
812 | },
813 | yAxis: {
814 | type: 'category',
815 | data: displayData.map(item => item.name),
816 | axisLabel: {
817 | show: true,
818 | fontSize: 11,
819 | formatter: (value, index) => this._formatValueWithUnits(displayData[index].value, displayData[index].units),
820 | align: 'right'
821 | },
822 | axisTick: { show: false },
823 | axisLine: { show: false },
824 | splitLine: { show: false }
825 | },
826 | series: [{
827 | type: 'bar',
828 | data: displayData.map((item, index) => ({
829 | ...item,
830 | itemStyle: {
831 | color: this._getColorByIndex(index),
832 | borderColor: '#e0e0e0',
833 | borderWidth: 1
834 | }
835 | })),
836 | label: {
837 | show: true,
838 | position: 'insideLeft',
839 | formatter: params => {
840 | const maxLength = 30;
841 | let label = params.name;
842 | if (label.length > maxLength) {
843 | label = label.substring(0, maxLength - 3) + '...';
844 | }
845 | return label;
846 | },
847 | fontSize: 11,
848 | distance: 5,
849 | color: '#000000'
850 | },
851 | barCategoryGap: 1,
852 | barGap: 0,
853 | barWidth: null
854 | }]
855 | };
856 |
857 | if (chartData.length > itemsPerPage && this._chart_container) {
858 | const existingLink = this._chart_container.querySelector('.show-more-link');
859 | if (existingLink) {
860 | existingLink.remove();
861 | }
862 |
863 | const showMoreLink = document.createElement('a');
864 | showMoreLink.href = '#';
865 | showMoreLink.textContent = 'Show more';
866 | showMoreLink.className = 'show-more-link';
867 | showMoreLink.style.cssText = `
868 | color: #1976d2;
869 | text-decoration: none;
870 | cursor: pointer;
871 | position: absolute;
872 | bottom: 5px;
873 | right: 5%;
874 | font-size: 11px;
875 | `;
876 |
877 | showMoreLink.onclick = (e) => {
878 | e.preventDefault();
879 | this._showMorePopup(chartData, itemsPerPage);
880 | };
881 |
882 | this._chart_container.appendChild(showMoreLink);
883 | }
884 |
885 | return chartOptions;
886 | }
887 |
888 | _showMorePopup(chartData, itemsPerPage) {
889 | const existingPopup = document.querySelector('.chart-popup-overlay');
890 | if (existingPopup) {
891 | existingPopup.remove();
892 | }
893 |
894 | const isDarkTheme = document.body.classList.contains('theme-dark');
895 | const themeColors = {
896 | light: {
897 | background: '#ffffff',
898 | text: '#1f2c33',
899 | border: '#dfe4e7',
900 | headerBg: '#f8f8f8'
901 | },
902 | dark: {
903 | background: '#0f1215',
904 | text: '#ebeef0',
905 | border: '#383838',
906 | headerBg: '#1c2026'
907 | }
908 | };
909 | const colors = isDarkTheme ? themeColors.dark : themeColors.light;
910 |
911 | const overlay = document.createElement('div');
912 | overlay.className = 'chart-popup-overlay';
913 | overlay.style.cssText = `
914 | position: fixed;
915 | top: 0;
916 | left: 0;
917 | right: 0;
918 | bottom: 0;
919 | background: rgba(0, 0, 0, 0.5);
920 | display: flex;
921 | justify-content: center;
922 | align-items: center;
923 | z-index: 1000;
924 | `;
925 |
926 | const popup = document.createElement('div');
927 | popup.className = 'chart-popup';
928 | popup.style.cssText = `
929 | background: ${colors.background};
930 | padding: 20px;
931 | border-radius: 2px;
932 | max-width: 800px;
933 | width: 90%;
934 | max-height: 80vh;
935 | overflow-y: auto;
936 | position: relative;
937 | color: ${colors.text};
938 | border: 1px solid ${colors.border};
939 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
940 | `;
941 |
942 | const closeButton = document.createElement('button');
943 | closeButton.textContent = '×';
944 | closeButton.style.cssText = `
945 | position: absolute;
946 | right: 10px;
947 | top: 10px;
948 | border: none;
949 | background: none;
950 | font-size: 24px;
951 | cursor: pointer;
952 | padding: 0 8px;
953 | color: ${colors.text};
954 | opacity: 0.7;
955 | transition: opacity 0.2s;
956 | `;
957 | closeButton.onmouseover = () => closeButton.style.opacity = '1';
958 | closeButton.onmouseout = () => closeButton.style.opacity = '0.7';
959 | closeButton.onclick = () => overlay.remove();
960 |
961 | const table = document.createElement('table');
962 | table.style.cssText = `
963 | width: 100%;
964 | border-collapse: collapse;
965 | margin-top: 20px;
966 | font-size: 11px;
967 | `;
968 |
969 | const thead = document.createElement('thead');
970 | thead.innerHTML = `
971 |
972 | Name |
973 | Value |
974 |
975 | `;
976 | table.appendChild(thead);
977 |
978 | const tbody = document.createElement('tbody');
979 | chartData.forEach((item, index) => {
980 | if (index >= itemsPerPage) {
981 | const row = document.createElement('tr');
982 | row.style.cssText = `
983 | border-bottom: 1px solid ${colors.border};
984 | transition: background-color 0.2s;
985 | `;
986 | row.onmouseover = () => row.style.backgroundColor = isDarkTheme ? '#2f3236' : '#f2f4f5';
987 | row.onmouseout = () => row.style.backgroundColor = 'transparent';
988 |
989 | row.innerHTML = `
990 | ${item.name} |
991 | ${this._formatValueWithUnits(item.value, item.units)} |
992 | `;
993 | tbody.appendChild(row);
994 | }
995 | });
996 | table.appendChild(thead);
997 | table.appendChild(tbody);
998 |
999 | popup.appendChild(closeButton);
1000 | popup.appendChild(table);
1001 | overlay.appendChild(popup);
1002 | document.body.appendChild(overlay);
1003 |
1004 | overlay.onclick = (e) => {
1005 | if (e.target === overlay) {
1006 | overlay.remove();
1007 | }
1008 | };
1009 | }
1010 |
1011 | _createGaugeChart(data) {
1012 | if (!data.fields || !data.fields.length) {
1013 | return null;
1014 | }
1015 |
1016 | const field = data.fields[0];
1017 | const value = parseFloat(field.value);
1018 |
1019 | if (isNaN(value)) {
1020 | return null;
1021 | }
1022 |
1023 | return {
1024 | tooltip: {
1025 | formatter: (params) => {
1026 | return `${field.name}: ${value.toFixed(2)}${field.units || ''}`;
1027 | }
1028 | },
1029 | grid: {
1030 | top: 8,
1031 | bottom: 8,
1032 | left: 8,
1033 | right: 8,
1034 | containLabel: true
1035 | },
1036 | series: [{
1037 | type: 'gauge',
1038 | startAngle: 180,
1039 | endAngle: 0,
1040 | min: 0,
1041 | max: 100,
1042 | radius: '80%',
1043 | center: ['50%', '65%'],
1044 | splitNumber: 5,
1045 | axisLine: {
1046 | lineStyle: {
1047 | width: 3,
1048 | color: [
1049 | [0.3, '#91cc75'], // 0-30% verde
1050 | [0.7, '#fac858'], // 30-70% amarelo
1051 | [1, '#ee6666'] // 70-100% vermelho
1052 | ]
1053 | }
1054 | },
1055 | pointer: {
1056 | icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z',
1057 | length: '45%',
1058 | width: 4,
1059 | offsetCenter: [0, 0],
1060 | itemStyle: {
1061 | color: 'auto'
1062 | }
1063 | },
1064 | axisTick: {
1065 | distance: -8,
1066 | length: 4,
1067 | lineStyle: {
1068 | color: '#fff',
1069 | width: 1
1070 | }
1071 | },
1072 | splitLine: {
1073 | distance: -10,
1074 | length: 6,
1075 | lineStyle: {
1076 | color: '#fff',
1077 | width: 1
1078 | }
1079 | },
1080 | axisLabel: {
1081 | color: '#999',
1082 | distance: -16,
1083 | fontSize: 8,
1084 | formatter: (value) => {
1085 | return value + '%';
1086 | }
1087 | },
1088 | anchor: {
1089 | show: true,
1090 | showAbove: true,
1091 | size: 10,
1092 | itemStyle: {
1093 | color: '#999'
1094 | }
1095 | },
1096 | title: {
1097 | show: true,
1098 | offsetCenter: [0, '30%'],
1099 | fontSize: 10,
1100 | color: '#999'
1101 | },
1102 | detail: {
1103 | valueAnimation: true,
1104 | fontSize: 16,
1105 | offsetCenter: [0, '8%'],
1106 | formatter: (value) => {
1107 | return value.toFixed(2) + '%';
1108 | },
1109 | color: 'inherit'
1110 | },
1111 | data: [{
1112 | value: value,
1113 | name: field.name
1114 | }]
1115 | }]
1116 | };
1117 | }
1118 |
1119 | _createLiquidChart(data) {
1120 | if (!data.fields || !data.fields.length) {
1121 | console.error('Sem dados para criar gráfico Liquid');
1122 | return null;
1123 | }
1124 |
1125 | // Preparar dados para o gráfico
1126 | const items = data.fields;
1127 | const waves = [];
1128 |
1129 |
1130 | // Limitar a 3 itens para melhor visualização
1131 | const maxItems = Math.min(items.length, 3);
1132 |
1133 | for (let i = 0; i < maxItems; i++) {
1134 | const item = items[i];
1135 | const value = parseFloat(item.value);
1136 |
1137 | if (isNaN(value)) {
1138 | continue;
1139 | }
1140 |
1141 | // Normalizar o valor para 0-1 para uso no liquid fill
1142 | const normalizedValue = Math.max(0, Math.min(1, value / 100));
1143 |
1144 | // Adicionar a camada com opacidade decrescente para camadas mais profundas
1145 | const opacity = 0.95 - (i * 0.2);
1146 |
1147 | waves.push({
1148 | value: normalizedValue,
1149 | itemStyle: {
1150 | color: this._getColorByIndex(i),
1151 | opacity: opacity
1152 | },
1153 | // Adicionar dados originais para o tooltip
1154 | originalValue: value,
1155 | originalName: item.name,
1156 | originalUnits: item.units
1157 | });
1158 | }
1159 |
1160 | if (waves.length === 0) {
1161 | console.error('Nenhum item válido para exibir no gráfico Liquid');
1162 | return null;
1163 | }
1164 |
1165 | // Remover qualquer legenda personalizada anterior
1166 | if (this._chart_container) {
1167 | const existingLegend = this._chart_container.querySelector('.liquid-legend');
1168 | if (existingLegend) {
1169 | existingLegend.remove();
1170 | }
1171 | }
1172 |
1173 | // Se tivermos apenas um item, usar configuração simplificada
1174 | if (waves.length === 1) {
1175 | const item = items[0];
1176 | const value = parseFloat(item.value);
1177 | const formattedValue = this._formatValueWithUnits(value, item.units);
1178 |
1179 | return {
1180 | tooltip: {
1181 | formatter: (params) => {
1182 | const itemData = items[0];
1183 | return `${itemData.name}: ${this._formatValueWithUnits(itemData.value, itemData.units)}`;
1184 | }
1185 | },
1186 | series: [{
1187 | type: 'liquidFill',
1188 | data: waves,
1189 | radius: '80%',
1190 | center: ['50%', '50%'],
1191 | outline: {
1192 | show: true,
1193 | borderDistance: 5,
1194 | itemStyle: {
1195 | borderColor: '#294D99',
1196 | borderWidth: 2
1197 | }
1198 | },
1199 | backgroundStyle: {
1200 | color: 'rgba(0, 0, 0, 0.1)'
1201 | },
1202 | label: {
1203 | show: true,
1204 | fontSize: 30,
1205 | fontWeight: 'bold',
1206 | formatter: () => formattedValue,
1207 | color: '#fff'
1208 | },
1209 | itemStyle: {
1210 | shadowBlur: 30,
1211 | shadowColor: 'rgba(0, 0, 0, 0.4)'
1212 | }
1213 | }]
1214 | };
1215 | }
1216 |
1217 | // Para múltiplos itens, mostrar valor do principal no centro
1218 | const primaryItem = items[0];
1219 | const primaryValue = parseFloat(primaryItem.value);
1220 | const formattedValue = this._formatValueWithUnits(primaryValue, primaryItem.units);
1221 |
1222 | // Adicionar uma legenda para múltiplos itens
1223 | if (this._chart_container && waves.length > 1) {
1224 | // Criar legenda personalizada
1225 | setTimeout(() => {
1226 | // Garantir que o container ainda exista
1227 | if (!this._chart_container) return;
1228 |
1229 | // Criar a legenda
1230 | const legend = document.createElement('div');
1231 | legend.className = 'liquid-legend';
1232 | legend.style.cssText = `
1233 | position: absolute;
1234 | bottom: 10px;
1235 | left: 0;
1236 | right: 0;
1237 | text-align: center;
1238 | font-size: 12px;
1239 | color: white;
1240 | z-index: 10;
1241 | background-color: rgba(0,0,0,0.2);
1242 | padding: 5px;
1243 | border-radius: 4px;
1244 | pointer-events: none;
1245 | `;
1246 |
1247 | // Adicionar itens à legenda
1248 | for (let i = 0; i < items.length && i < maxItems; i++) {
1249 | const item = items[i];
1250 | const formattedItemValue = this._formatValueWithUnits(item.value, item.units);
1251 | const color = this._getColorByIndex(i);
1252 |
1253 | const legendItem = document.createElement('div');
1254 | legendItem.style.cssText = `
1255 | display: inline-block;
1256 | margin: 0 5px;
1257 | white-space: nowrap;
1258 | `;
1259 |
1260 | legendItem.innerHTML = `
1261 |
1262 | ${item.name}: ${formattedItemValue}
1263 | `;
1264 |
1265 | legend.appendChild(legendItem);
1266 | }
1267 |
1268 | this._chart_container.appendChild(legend);
1269 | }, 100);
1270 | }
1271 |
1272 | return {
1273 | tooltip: {
1274 | formatter: (params) => {
1275 | // Determinar qual item do tooltip está sendo mostrado com base no índice
1276 | const waveIndex = params.dataIndex;
1277 | if (waveIndex >= 0 && waveIndex < items.length) {
1278 | const itemData = items[waveIndex];
1279 | return `${itemData.name}: ${this._formatValueWithUnits(itemData.value, itemData.units)}`;
1280 | }
1281 | return "Sem dados";
1282 | }
1283 | },
1284 | series: [{
1285 | type: 'liquidFill',
1286 | data: waves, // Múltiplos items como camadas no mesmo gráfico
1287 | radius: '80%',
1288 | center: ['50%', '50%'],
1289 | outline: {
1290 | show: true,
1291 | borderDistance: 5,
1292 | itemStyle: {
1293 | borderColor: '#294D99',
1294 | borderWidth: 2
1295 | }
1296 | },
1297 | backgroundStyle: {
1298 | color: 'rgba(0, 0, 0, 0.1)'
1299 | },
1300 | label: {
1301 | show: true,
1302 | position: 'inside',
1303 | formatter: () => formattedValue,
1304 | fontSize: 28,
1305 | fontWeight: 'bold',
1306 | color: '#fff'
1307 | },
1308 | amplitude: 20,
1309 | waveLength: '80%',
1310 | phase: 'auto',
1311 | period: 'auto',
1312 | direction: 'right',
1313 | waveAnimation: true,
1314 | animationEasing: 'linear',
1315 | animationEasingUpdate: 'linear',
1316 | animationDuration: 2000,
1317 | animationDurationUpdate: 1000
1318 | }]
1319 | };
1320 | }
1321 |
1322 | _createPieChart(data) {
1323 | const fields = data.fields;
1324 | if (!fields || !fields.length) return null;
1325 |
1326 | // Determinar a cor do tema atual
1327 | const isDarkTheme = document.body.classList.contains('dark-theme');
1328 | const textColor = isDarkTheme ? '#ffffff' : '#000000';
1329 |
1330 | // Preparar dados para o gráfico de pizza
1331 | const chartData = fields.map((field, index) => ({
1332 | value: parseFloat(field.value),
1333 | name: field.name,
1334 | units: field.units || '',
1335 | itemStyle: {
1336 | color: this._getColorByIndex(index)
1337 | }
1338 | })).filter(item => !isNaN(item.value));
1339 |
1340 | if (chartData.length === 0) return null;
1341 |
1342 | // Se tivermos apenas um item, mostrar como "valor vs. 100-valor"
1343 | if (chartData.length === 1) {
1344 | const item = chartData[0];
1345 | const remaining = 100 - item.value;
1346 |
1347 | return {
1348 | tooltip: {
1349 | trigger: 'item',
1350 | formatter: (params) => {
1351 | if (params.name === 'Remaining') {
1352 | return 'Remaining: ' + remaining.toFixed(2) + '%';
1353 | }
1354 | return `${params.name}: ${this._formatValueWithUnits(params.value, item.units)}`;
1355 | }
1356 | },
1357 | legend: {
1358 | orient: 'vertical',
1359 | left: 'left',
1360 | data: [item.name, 'Remaining'],
1361 | textStyle: {
1362 | color: textColor
1363 | }
1364 | },
1365 | series: [{
1366 | name: item.name,
1367 | type: 'pie',
1368 | radius: ['40%', '70%'],
1369 | avoidLabelOverlap: false,
1370 | itemStyle: {
1371 | borderRadius: 10,
1372 | borderColor: '#fff',
1373 | borderWidth: 2
1374 | },
1375 | label: {
1376 | show: true,
1377 | formatter: (params) => {
1378 | if (params.name === 'Remaining') {
1379 | return params.value.toFixed(2) + '%';
1380 | }
1381 | return this._formatValueWithUnits(params.value, item.units);
1382 | },
1383 | color: textColor
1384 | },
1385 | data: [
1386 | { value: item.value, name: item.name, itemStyle: { color: this._getColorByIndex(0) } },
1387 | { value: remaining, name: 'Remaining', itemStyle: { color: '#999' } }
1388 | ]
1389 | }]
1390 | };
1391 | }
1392 |
1393 | // Se tivermos múltiplos itens, mostrar todos em um gráfico de pizza
1394 | return {
1395 | tooltip: {
1396 | trigger: 'item',
1397 | formatter: (params) => {
1398 | const item = chartData.find(i => i.name === params.name);
1399 | if (item) {
1400 | return `${params.name}: ${this._formatValueWithUnits(params.value, item.units)}`;
1401 | }
1402 | return `${params.name}: ${params.value}`;
1403 | }
1404 | },
1405 | legend: {
1406 | orient: 'vertical',
1407 | left: 'left',
1408 | data: chartData.map(item => item.name),
1409 | textStyle: {
1410 | color: textColor
1411 | }
1412 | },
1413 | series: [{
1414 | type: 'pie',
1415 | radius: ['30%', '70%'],
1416 | center: ['55%', '50%'],
1417 | roseType: false,
1418 | itemStyle: {
1419 | borderRadius: 10,
1420 | borderColor: '#fff',
1421 | borderWidth: 2
1422 | },
1423 | label: {
1424 | show: true,
1425 | formatter: (params) => {
1426 | const item = chartData.find(i => i.name === params.name);
1427 | if (item) {
1428 | return this._formatValueWithUnits(params.value, item.units);
1429 | }
1430 | return params.value;
1431 | },
1432 | color: textColor
1433 | },
1434 | emphasis: {
1435 | label: {
1436 | show: true,
1437 | fontSize: 14,
1438 | fontWeight: 'bold',
1439 | color: textColor
1440 | }
1441 | },
1442 | data: chartData
1443 | }]
1444 | };
1445 | }
1446 |
1447 | _createTreemapChart(data) {
1448 | // Prepare treemap data structure
1449 | const items = data.fields;
1450 |
1451 | if (!items || !items.length) {
1452 | return null;
1453 | }
1454 |
1455 | // Get unit type configuration
1456 | const unitType = parseInt(this._fields_values.unit_type || WidgetEcharts.UNIT_TYPE_NONE);
1457 |
1458 | // Group items by host if available
1459 | const treeData = [];
1460 | const hostGroups = {};
1461 |
1462 | // First, group by host
1463 | items.forEach(item => {
1464 | const host = item.host || 'Unknown';
1465 | if (!hostGroups[host]) {
1466 | hostGroups[host] = {
1467 | name: host,
1468 | children: []
1469 | };
1470 | treeData.push(hostGroups[host]);
1471 | }
1472 |
1473 | // Add item to host group with appropriate value formatting
1474 | const value = Math.abs(parseFloat(item.value));
1475 | hostGroups[host].children.push({
1476 | name: item.name.replace('Container /', ''), // Remove o prefixo "Container /" para melhor visualização
1477 | value: value,
1478 | itemId: item.id,
1479 | rawValue: value, // Store raw value for tooltip
1480 | units: item.units // Armazena as unidades para uso no tooltip
1481 | });
1482 | });
1483 |
1484 | // Function to define levels appearance
1485 | function getLevelOption() {
1486 | return [
1487 | {
1488 | itemStyle: {
1489 | borderWidth: 0,
1490 | gapWidth: 5
1491 | }
1492 | },
1493 | {
1494 | itemStyle: {
1495 | gapWidth: 1
1496 | }
1497 | }
1498 | ];
1499 | }
1500 |
1501 | // Encontrar valor mínimo e máximo para a escala de cores
1502 | const allValues = [];
1503 | treeData.forEach(host => {
1504 | host.children.forEach(item => {
1505 | allValues.push(item.value);
1506 | });
1507 | });
1508 | const minValue = Math.min(...allValues);
1509 | const maxValue = Math.max(...allValues);
1510 |
1511 | // Adicionar cores aos dados
1512 | let colorIndex = 0;
1513 | treeData.forEach(host => {
1514 | host.children.forEach(item => {
1515 | item.itemStyle = {
1516 | color: this._getColorByIndex(colorIndex++)
1517 | };
1518 | });
1519 | });
1520 |
1521 | return {
1522 | tooltip: {
1523 | formatter: info => {
1524 | const value = info.data.rawValue;
1525 | const treePathInfo = info.treePathInfo;
1526 | const treePath = [];
1527 |
1528 | for (let i = 1; i < treePathInfo.length; i++) {
1529 | treePath.push(treePathInfo[i].name);
1530 | }
1531 |
1532 | // Format value based on unit type and stored units
1533 | let formattedValue = this._formatValueWithUnits(value, info.data.units);
1534 |
1535 | return [
1536 | '' +
1537 | treePath.join(' / ') +
1538 | '
',
1539 | 'Valor: ' + formattedValue
1540 | ].join('');
1541 | }
1542 | },
1543 | series: [{
1544 | name: 'Metrics',
1545 | type: 'treemap',
1546 | top: 30,
1547 | bottom: 10,
1548 | left: 10,
1549 | right: 10,
1550 | roam: 'scale',
1551 | nodeClick: true,
1552 | breadcrumb: {
1553 | show: true,
1554 | height: 25,
1555 | top: 0,
1556 | left: 10,
1557 | right: 10,
1558 | emptyItemWidth: 25,
1559 | itemStyle: {
1560 | color: 'rgba(255, 255, 255, 0.7)',
1561 | borderColor: 'rgba(255, 255, 255, 0.7)',
1562 | borderWidth: 1,
1563 | textStyle: {
1564 | color: '#fff'
1565 | }
1566 | }
1567 | },
1568 | visualMin: minValue,
1569 | visualMax: maxValue,
1570 | label: {
1571 | show: true,
1572 | formatter: params => {
1573 | const name = params.name.replace('Container /', '');
1574 | const value = this._formatValueWithUnits(params.value, params.data.units);
1575 |
1576 | // Ajusta o tamanho do texto baseado no tamanho do retângulo
1577 | const rectArea = params.area;
1578 | const isSmallRect = rectArea < 2000; // Ajuste este valor conforme necessário
1579 |
1580 | if (isSmallRect) {
1581 | // Para retângulos pequenos, mostra apenas o valor
1582 | return value;
1583 | }
1584 |
1585 | // Para retângulos maiores, mostra nome e valor
1586 | return name + '\n' + value;
1587 | },
1588 | ellipsis: true,
1589 | fontSize: 11,
1590 | color: '#fff',
1591 | rich: {
1592 | value: {
1593 | fontSize: 10,
1594 | lineHeight: 14,
1595 | color: 'rgba(255, 255, 255, 0.9)'
1596 | }
1597 | }
1598 | },
1599 | itemStyle: {
1600 | borderColor: '#fff',
1601 | borderWidth: 1,
1602 | gapWidth: 1,
1603 | borderRadius: 2
1604 | },
1605 | emphasis: {
1606 | label: {
1607 | show: true,
1608 | fontSize: 12,
1609 | fontWeight: 'bold'
1610 | },
1611 | itemStyle: {
1612 | borderWidth: 2
1613 | }
1614 | },
1615 | levels: getLevelOption(),
1616 | data: treeData,
1617 | animation: true,
1618 | animationDuration: 500,
1619 | animationEasing: 'cubicOut'
1620 | }]
1621 | };
1622 | }
1623 |
1624 | _createRoseChart(data) {
1625 | const items = data.fields;
1626 | if (!items || !items.length) {
1627 | return null;
1628 | }
1629 |
1630 | // Preparar dados para o gráfico
1631 | const chartData = items.map((item, index) => ({
1632 | value: Math.abs(parseFloat(item.value)),
1633 | name: item.name,
1634 | itemStyle: {
1635 | color: this._getColorByIndex(index)
1636 | }
1637 | }));
1638 |
1639 | // Calcular o raio baseado no tamanho do container
1640 | const containerWidth = this._chart_container.clientWidth;
1641 | const containerHeight = this._chart_container.clientHeight;
1642 | const minDimension = Math.min(containerWidth, containerHeight);
1643 | const maxRadius = Math.floor(minDimension * 0.4); // 40% do menor lado
1644 | const minRadius = Math.floor(maxRadius * 0.2); // 20% do raio máximo
1645 |
1646 | return {
1647 | legend: {
1648 | type: 'scroll',
1649 | orient: 'horizontal',
1650 | bottom: 10,
1651 | textStyle: {
1652 | fontSize: 11
1653 | }
1654 | },
1655 | tooltip: {
1656 | trigger: 'item',
1657 | formatter: params => {
1658 | const item = items.find(i => i.name === params.name);
1659 | if (!item) return params.name;
1660 |
1661 | let value = this._formatValueWithUnits(params.value, item.units);
1662 | let unitSuffix = '';
1663 |
1664 | const unitType = parseInt(this._fields_values.unit_type || WidgetEcharts.UNIT_TYPE_NONE);
1665 | if (unitType === WidgetEcharts.UNIT_TYPE_PERCENTAGE) {
1666 | if (value.endsWith('%')) {
1667 | value = value.replace(/%$/, '');
1668 | }
1669 | unitSuffix = '%';
1670 | } else if (unitType === WidgetEcharts.UNIT_TYPE_BITS) {
1671 | unitSuffix = 'bps';
1672 | } else if (item.units) {
1673 | if (!value.endsWith(item.units)) {
1674 | unitSuffix = item.units;
1675 | }
1676 | }
1677 |
1678 | return `${params.name}: ${value} ${unitSuffix}`;
1679 | }
1680 | },
1681 | toolbox: {
1682 | show: true,
1683 | feature: {
1684 | saveAsImage: {
1685 | show: true,
1686 | title: 'Save as Image'
1687 | }
1688 | },
1689 | right: 20,
1690 | top: 0
1691 | },
1692 | series: [{
1693 | name: 'Metrics',
1694 | type: 'pie',
1695 | radius: [minRadius, maxRadius],
1696 | center: ['50%', '50%'],
1697 | roseType: 'area',
1698 | itemStyle: {
1699 | borderRadius: 4,
1700 | borderColor: '#fff',
1701 | borderWidth: 1
1702 | },
1703 | label: {
1704 | show: true,
1705 | formatter: params => {
1706 | const item = items.find(i => i.name === params.name);
1707 | if (!item) return params.name;
1708 |
1709 | let value = this._formatValueWithUnits(params.value, item.units);
1710 | let unitSuffix = '';
1711 |
1712 | const unitType = parseInt(this._fields_values.unit_type || WidgetEcharts.UNIT_TYPE_NONE);
1713 | if (unitType === WidgetEcharts.UNIT_TYPE_PERCENTAGE) {
1714 | if (value.endsWith('%')) {
1715 | value = value.replace(/%$/, '');
1716 | }
1717 | unitSuffix = '%';
1718 | } else if (unitType === WidgetEcharts.UNIT_TYPE_BITS) {
1719 | unitSuffix = 'bps';
1720 | } else if (item.units) {
1721 | if (!value.endsWith(item.units)) {
1722 | unitSuffix = item.units;
1723 | }
1724 | }
1725 |
1726 | return `${value} ${unitSuffix}`;
1727 | },
1728 | fontSize: 11
1729 | },
1730 | data: chartData
1731 | }]
1732 | };
1733 | }
1734 |
1735 | _createLLDTableChart(data) {
1736 |
1737 |
1738 | if (!this._chart_container || !data.fields || !data.fields.length) {
1739 | console.warn('No data or container available for LLD table', {
1740 | container: !!this._chart_container,
1741 | fields: !!data.fields,
1742 | length: data.fields?.length
1743 | });
1744 | return {
1745 | series: [],
1746 | tooltip: {
1747 | show: false
1748 | }
1749 | };
1750 | }
1751 |
1752 | // Clear and set up the container
1753 | this._chart_container.innerHTML = '';
1754 | this._chart_container.style.cssText = `
1755 | position: absolute;
1756 | top: 0;
1757 | left: 0;
1758 | right: 0;
1759 | bottom: 0;
1760 | display: flex;
1761 | flex-direction: column;
1762 | padding: 0;
1763 | margin: 0;
1764 | overflow: hidden;
1765 | background: var(--widget-bg-color);
1766 | `;
1767 |
1768 | // Create table container for scrolling
1769 | const tableContainer = document.createElement('div');
1770 | tableContainer.style.cssText = `
1771 | flex: 1;
1772 | overflow: auto;
1773 | margin: 0;
1774 | padding: 0;
1775 | width: 100%;
1776 | `;
1777 |
1778 | // Process data
1779 | const lldData = new Map();
1780 | const metrics = new Set();
1781 | const columnUnits = this._fields_values.column_units || { columns: [], units: [] };
1782 |
1783 | data.fields.forEach(field => {
1784 | const parts = field.name.split(': ');
1785 | if (parts.length < 2) return;
1786 |
1787 | const metricName = parts[parts.length - 1];
1788 | const entityName = parts.slice(0, -1).join(': ');
1789 |
1790 | metrics.add(metricName);
1791 |
1792 | if (!lldData.has(entityName)) {
1793 | lldData.set(entityName, new Map());
1794 | }
1795 |
1796 | lldData.get(entityName).set(metricName, {
1797 | value: field.value,
1798 | units: field.units,
1799 | valuemapid: field.valuemapid,
1800 | name: field.name,
1801 | itemid: field.itemid
1802 | });
1803 | });
1804 |
1805 | const metricsList = Array.from(metrics).sort();
1806 | let sortedEntities = Array.from(lldData.entries());
1807 |
1808 | // Estado de ordenação
1809 | if (!this._sortState) {
1810 | this._sortState = {
1811 | column: metricsList[0], // Primeira métrica por padrão
1812 | direction: 'desc' // Descendente por padrão
1813 | };
1814 | }
1815 |
1816 | // Função de ordenação
1817 | const sortData = (column, direction) => {
1818 | this._sortState.column = column;
1819 | this._sortState.direction = direction;
1820 |
1821 | if (column === 'name') {
1822 | sortedEntities.sort((a, b) => {
1823 | return direction === 'asc' ?
1824 | a[0].localeCompare(b[0]) :
1825 | b[0].localeCompare(a[0]);
1826 | });
1827 | } else {
1828 | sortedEntities.sort((a, b) => {
1829 | const valueA = parseFloat(a[1].get(column)?.value || 0);
1830 | const valueB = parseFloat(b[1].get(column)?.value || 0);
1831 | return direction === 'asc' ? valueA - valueB : valueB - valueA;
1832 | });
1833 | }
1834 | };
1835 |
1836 | // Ordenação inicial
1837 | sortData(this._sortState.column, this._sortState.direction);
1838 |
1839 | // Create table
1840 | const table = document.createElement('table');
1841 | table.className = 'list-table';
1842 | table.style.cssText = `
1843 | width: 100%;
1844 | border-collapse: collapse;
1845 | font-size: 11px;
1846 | margin: 0;
1847 | padding: 0;
1848 | table-layout: fixed;
1849 | `;
1850 |
1851 | // Create header
1852 | const thead = document.createElement('thead');
1853 | const headerRow = document.createElement('tr');
1854 |
1855 | // Calculate column widths
1856 | const totalColumns = metricsList.length + 1;
1857 | const nameColumnWidth = '30%';
1858 | const metricColumnWidth = `${70 / metricsList.length}%`;
1859 |
1860 | // Função para criar seta de ordenação
1861 | const createSortArrow = (column) => {
1862 | const arrow = document.createElement('span');
1863 | arrow.style.cssText = `
1864 | margin-left: 5px;
1865 | opacity: ${column === this._sortState.column ? '1' : '0.3'};
1866 | `;
1867 | arrow.textContent = this._sortState.direction === 'asc' ? '↑' : '↓';
1868 | return arrow;
1869 | };
1870 |
1871 | // Add Name column header
1872 | const nameHeader = document.createElement('th');
1873 | nameHeader.style.cssText = `
1874 | position: sticky;
1875 | top: 0;
1876 | background: var(--widget-bg-color);
1877 | padding: 5px;
1878 | text-align: left;
1879 | border-bottom: 1px solid var(--border-color);
1880 | z-index: 1;
1881 | width: ${nameColumnWidth};
1882 | white-space: nowrap;
1883 | overflow: hidden;
1884 | text-overflow: ellipsis;
1885 | cursor: pointer;
1886 | `;
1887 |
1888 | const nameHeaderText = document.createElement('span');
1889 | nameHeaderText.textContent = 'Name';
1890 | nameHeader.appendChild(nameHeaderText);
1891 | nameHeader.appendChild(createSortArrow('name'));
1892 |
1893 | nameHeader.onclick = () => {
1894 | const newDirection = this._sortState.column === 'name' && this._sortState.direction === 'asc' ? 'desc' : 'asc';
1895 | sortData('name', newDirection);
1896 | updateTableContent(currentPage);
1897 | updateHeaders();
1898 | };
1899 |
1900 | headerRow.appendChild(nameHeader);
1901 |
1902 | // Função para atualizar cabeçalhos
1903 | const updateHeaders = () => {
1904 | const headers = headerRow.querySelectorAll('th');
1905 | headers.forEach(header => {
1906 | const arrow = header.querySelector('span:last-child');
1907 | if (arrow) {
1908 | arrow.style.opacity = header.dataset.column === this._sortState.column ? '1' : '0.3';
1909 | arrow.textContent = this._sortState.direction === 'asc' ? '↑' : '↓';
1910 | }
1911 | });
1912 | };
1913 |
1914 | // Add metric headers
1915 | metricsList.forEach(metric => {
1916 | const th = document.createElement('th');
1917 | th.dataset.column = metric;
1918 | th.style.cssText = `
1919 | position: sticky;
1920 | top: 0;
1921 | background: var(--widget-bg-color);
1922 | padding: 5px;
1923 | text-align: right;
1924 | border-bottom: 1px solid var(--border-color);
1925 | z-index: 1;
1926 | width: ${metricColumnWidth};
1927 | white-space: nowrap;
1928 | overflow: hidden;
1929 | text-overflow: ellipsis;
1930 | cursor: pointer;
1931 | `;
1932 |
1933 | const metricText = document.createElement('span');
1934 | metricText.textContent = metric;
1935 | th.appendChild(metricText);
1936 | th.appendChild(createSortArrow(metric));
1937 |
1938 | th.onclick = () => {
1939 | const newDirection = this._sortState.column === metric && this._sortState.direction === 'asc' ? 'desc' : 'asc';
1940 | sortData(metric, newDirection);
1941 | updateTableContent(currentPage);
1942 | updateHeaders();
1943 | };
1944 |
1945 | headerRow.appendChild(th);
1946 | });
1947 |
1948 | thead.appendChild(headerRow);
1949 | table.appendChild(thead);
1950 |
1951 | // Create table body
1952 | const tbody = document.createElement('tbody');
1953 |
1954 | // Pagination variables
1955 | const itemsPerPage = 10;
1956 | let currentPage = 1;
1957 | const totalPages = Math.ceil(sortedEntities.length / itemsPerPage);
1958 |
1959 | // Function to format value based on column unit type
1960 | const formatValueByColumn = (value, metricName) => {
1961 | if (value === null || value === undefined) {
1962 | return 'N/A';
1963 | }
1964 |
1965 | try {
1966 | // Verificar primeiro se é notação científica
1967 | if (typeof value === 'string' && value.match(/^\d+\.\d+e[+-]\d+/)) {
1968 | // Encontra os dados da métrica para obter as unidades
1969 | let metricData = null;
1970 | for (const [entityName, metricsMap] of sortedEntities) {
1971 | if (metricsMap.has(metricName)) {
1972 | metricData = metricsMap.get(metricName);
1973 | break;
1974 | }
1975 | }
1976 |
1977 | // Se encontrou a métrica, usa o método específico para LLD
1978 | if (metricData) {
1979 | return this._formatLLDTableValue(value, metricData.units);
1980 | }
1981 |
1982 | // Se o valor parece ser "notação bps" (como 1.83e+8 bps)
1983 | if (value.includes('bps')) {
1984 | const parts = value.split(' ');
1985 | const numValue = Number(parts[0]);
1986 | if (!isNaN(numValue)) {
1987 | return this._formatBitsValue(numValue);
1988 | }
1989 | }
1990 |
1991 | // Para outros valores científicos
1992 | const numValue = Number(value);
1993 | if (!isNaN(numValue)) {
1994 | return this._formatNumberBySize(numValue);
1995 | }
1996 | }
1997 |
1998 | // Para valores numéricos normais
1999 | const numValue = parseFloat(value);
2000 | if (isNaN(numValue)) {
2001 | return value;
2002 | }
2003 |
2004 | // Encontra os dados da métrica
2005 | let metricData = null;
2006 | for (const [entityName, metricsMap] of sortedEntities) {
2007 | if (metricsMap.has(metricName)) {
2008 | metricData = metricsMap.get(metricName);
2009 | break;
2010 | }
2011 | }
2012 |
2013 | if (!metricData) {
2014 | return numValue.toFixed(2);
2015 | }
2016 |
2017 | const units = metricData.units;
2018 |
2019 | // Usar nosso método genérico para formatação
2020 | return this._formatValueWithUnits(numValue, units);
2021 |
2022 | } catch (error) {
2023 | console.error('Error formatting LLD value:', error);
2024 | return 'Error';
2025 | }
2026 | };
2027 |
2028 | // Function to update table content
2029 | const updateTableContent = (page) => {
2030 | tbody.innerHTML = '';
2031 |
2032 | const startIndex = (page - 1) * itemsPerPage;
2033 | const endIndex = Math.min(startIndex + itemsPerPage, sortedEntities.length);
2034 |
2035 | for (let i = startIndex; i < endIndex; i++) {
2036 | const [entityName, metricsMap] = sortedEntities[i];
2037 | const row = document.createElement('tr');
2038 |
2039 | // Add entity name cell
2040 | const nameCell = document.createElement('td');
2041 | nameCell.textContent = entityName;
2042 | nameCell.style.cssText = `
2043 | padding: 5px;
2044 | text-align: left;
2045 | border-bottom: 1px solid var(--border-color);
2046 | white-space: nowrap;
2047 | overflow: hidden;
2048 | text-overflow: ellipsis;
2049 | width: ${nameColumnWidth};
2050 | `;
2051 | row.appendChild(nameCell);
2052 |
2053 | // Add metric cells
2054 | metricsList.forEach(metric => {
2055 | const td = document.createElement('td');
2056 | const metricData = metricsMap.get(metric);
2057 |
2058 | let displayValue = 'N/A';
2059 |
2060 | if (metricData && metricData.value !== null && metricData.value !== undefined) {
2061 | displayValue = formatValueByColumn(metricData.value, metric);
2062 | }
2063 |
2064 | td.textContent = displayValue;
2065 | td.style.cssText = `
2066 | padding: 5px;
2067 | text-align: right;
2068 | border-bottom: 1px solid var(--border-color);
2069 | white-space: nowrap;
2070 | overflow: hidden;
2071 | text-overflow: ellipsis;
2072 | width: ${metricColumnWidth};
2073 | `;
2074 | row.appendChild(td);
2075 | });
2076 |
2077 | tbody.appendChild(row);
2078 | }
2079 | };
2080 |
2081 | table.appendChild(tbody);
2082 | tableContainer.appendChild(table);
2083 |
2084 | // Create pagination controls
2085 | const paginationDiv = document.createElement('div');
2086 | paginationDiv.style.cssText = `
2087 | padding: 5px;
2088 | text-align: center;
2089 | border-top: 1px solid var(--border-color);
2090 | background: var(--widget-bg-color);
2091 | flex-shrink: 0;
2092 | width: 100%;
2093 | `;
2094 |
2095 | const updatePaginationControls = () => {
2096 | paginationDiv.innerHTML = '';
2097 |
2098 | if (totalPages <= 1) return;
2099 |
2100 | const prevButton = document.createElement('button');
2101 | prevButton.textContent = '←';
2102 | prevButton.style.cssText = `
2103 | margin: 0 5px;
2104 | padding: 2px 8px;
2105 | cursor: pointer;
2106 | `;
2107 | prevButton.disabled = currentPage === 1;
2108 | prevButton.onclick = () => {
2109 | if (currentPage > 1) {
2110 | currentPage--;
2111 | updateTableContent(currentPage);
2112 | updatePaginationControls();
2113 | }
2114 | };
2115 | paginationDiv.appendChild(prevButton);
2116 |
2117 | const pageInfo = document.createElement('span');
2118 | pageInfo.style.margin = '0 10px';
2119 | pageInfo.textContent = `${currentPage} / ${totalPages}`;
2120 | paginationDiv.appendChild(pageInfo);
2121 |
2122 | const nextButton = document.createElement('button');
2123 | nextButton.textContent = '→';
2124 | nextButton.style.cssText = `
2125 | margin: 0 5px;
2126 | padding: 2px 8px;
2127 | cursor: pointer;
2128 | `;
2129 | nextButton.disabled = currentPage === totalPages;
2130 | nextButton.onclick = () => {
2131 | if (currentPage < totalPages) {
2132 | currentPage++;
2133 | updateTableContent(currentPage);
2134 | updatePaginationControls();
2135 | }
2136 | };
2137 | paginationDiv.appendChild(nextButton);
2138 | };
2139 |
2140 | // Initialize table content and controls
2141 | updateTableContent(1);
2142 | updatePaginationControls();
2143 |
2144 | // Append containers to chart container
2145 | this._chart_container.appendChild(tableContainer);
2146 | this._chart_container.appendChild(paginationDiv);
2147 |
2148 | return {
2149 | series: [],
2150 | tooltip: {
2151 | show: false
2152 | },
2153 | grid: {
2154 | show: false
2155 | }
2156 | };
2157 | }
2158 |
2159 | _createFunnelChart(data) {
2160 | const fields = data.fields;
2161 | if (!fields || !fields.length) return null;
2162 |
2163 | // Determinar a cor do tema atual
2164 | const isDarkTheme = document.body.classList.contains('dark-theme');
2165 | const textColor = isDarkTheme ? '#ffffff' : '#000000';
2166 |
2167 | // Preparar dados para o gráfico de funil
2168 | const chartData = fields.map((field, index) => ({
2169 | value: parseFloat(field.value),
2170 | name: field.name,
2171 | units: field.units || '',
2172 | itemStyle: {
2173 | color: this._getColorByIndex(index)
2174 | }
2175 | })).filter(item => !isNaN(item.value));
2176 |
2177 | if (chartData.length === 0) return null;
2178 |
2179 | return {
2180 | tooltip: {
2181 | trigger: 'item',
2182 | formatter: (params) => {
2183 | const item = chartData.find(i => i.name === params.name);
2184 | if (item) {
2185 | return `${params.name}: ${this._formatValueWithUnits(params.value, item.units)}`;
2186 | }
2187 | return `${params.name}: ${params.value}`;
2188 | }
2189 | },
2190 | legend: {
2191 | data: chartData.map(item => item.name),
2192 | textStyle: {
2193 | color: textColor
2194 | }
2195 | },
2196 | series: [{
2197 | type: 'funnel',
2198 | left: '10%',
2199 | top: 60,
2200 | bottom: 60,
2201 | width: '80%',
2202 | min: 0,
2203 | max: Math.max(...chartData.map(item => item.value)) * 1.1,
2204 | minSize: '0%',
2205 | maxSize: '100%',
2206 | sort: 'descending',
2207 | gap: 2,
2208 | label: {
2209 | show: true,
2210 | position: 'inside',
2211 | formatter: (params) => {
2212 | const item = chartData.find(i => i.name === params.name);
2213 | if (item) {
2214 | return `${params.name}: ${this._formatValueWithUnits(params.value, item.units)}`;
2215 | }
2216 | return `${params.name}: ${params.value}`;
2217 | },
2218 | color: textColor
2219 | },
2220 | labelLine: {
2221 | length: 10,
2222 | lineStyle: {
2223 | width: 1,
2224 | type: 'solid',
2225 | color: textColor
2226 | }
2227 | },
2228 | itemStyle: {
2229 | borderColor: '#fff',
2230 | borderWidth: 1
2231 | },
2232 | emphasis: {
2233 | label: {
2234 | fontSize: 14,
2235 | color: textColor
2236 | }
2237 | },
2238 | data: chartData
2239 | }]
2240 | };
2241 | }
2242 |
2243 | _createTreemapSunburstChart(data) {
2244 | if (!data.fields || !data.fields.length) {
2245 | return null;
2246 | }
2247 |
2248 | // Process data into hierarchical structure
2249 | const processedData = this._processDataForHierarchy(data.fields);
2250 |
2251 | // Generate random colors for each host
2252 | const colorMap = new Map();
2253 | processedData.forEach(host => {
2254 | colorMap.set(host.name, this._generateRandomColor());
2255 | host.children.forEach(child => {
2256 | colorMap.set(child.name, this._generateRandomColor());
2257 | });
2258 | });
2259 |
2260 | // Create treemap configuration
2261 | const treemapOption = {
2262 | series: [{
2263 | type: 'treemap',
2264 | id: 'metrics-hierarchy',
2265 | animationDurationUpdate: 2000,
2266 | roam: false,
2267 | nodeClick: undefined,
2268 | data: processedData.map(host => ({
2269 | ...host,
2270 | itemStyle: {
2271 | color: colorMap.get(host.name)
2272 | },
2273 | children: host.children.map(child => ({
2274 | ...child,
2275 | itemStyle: {
2276 | color: colorMap.get(child.name)
2277 | }
2278 | }))
2279 | })),
2280 | universalTransition: true,
2281 | label: {
2282 | show: true,
2283 | formatter: (params) => {
2284 | return params.name + '\n' + this._formatValueWithUnits(params.value, params.data.units);
2285 | },
2286 | fontSize: 11
2287 | },
2288 | breadcrumb: {
2289 | show: false
2290 | },
2291 | itemStyle: {
2292 | borderColor: '#fff',
2293 | borderWidth: 1
2294 | }
2295 | }]
2296 | };
2297 |
2298 | // Create sunburst configuration
2299 | const sunburstOption = {
2300 | series: [{
2301 | type: 'sunburst',
2302 | id: 'metrics-hierarchy',
2303 | radius: ['20%', '90%'],
2304 | animationDurationUpdate: 2000,
2305 | nodeClick: undefined,
2306 | data: processedData.map(host => ({
2307 | ...host,
2308 | itemStyle: {
2309 | color: colorMap.get(host.name)
2310 | },
2311 | children: host.children.map(child => ({
2312 | ...child,
2313 | itemStyle: {
2314 | color: colorMap.get(child.name)
2315 | }
2316 | }))
2317 | })),
2318 | universalTransition: true,
2319 | itemStyle: {
2320 | borderWidth: 1,
2321 | borderColor: 'rgba(255,255,255,.5)'
2322 | },
2323 | label: {
2324 | show: true,
2325 | formatter: (params) => {
2326 | const isLeaf = !params.data.children;
2327 | if (isLeaf) {
2328 | return this._formatValueWithUnits(params.value, params.data.units);
2329 | }
2330 | return params.name.length > 15 ? params.name.substring(0, 15) + '...' : params.name;
2331 | },
2332 | rotate: 'tangential',
2333 | fontSize: 10,
2334 | minAngle: 15
2335 | }
2336 | }]
2337 | };
2338 |
2339 | // Set initial option
2340 | let currentOption = treemapOption;
2341 | this._chart.setOption(currentOption);
2342 |
2343 | // Clear existing interval if any
2344 | if (this._chartTransitionInterval) {
2345 | clearInterval(this._chartTransitionInterval);
2346 | }
2347 |
2348 | // Set up transition interval
2349 | this._chartTransitionInterval = setInterval(() => {
2350 | currentOption = currentOption === treemapOption ? sunburstOption : treemapOption;
2351 | this._chart.setOption(currentOption);
2352 | }, 5000);
2353 |
2354 | return currentOption;
2355 | }
2356 |
2357 | _generateRandomColor() {
2358 | // Lista de cores base para garantir boa visibilidade
2359 | const baseColors = [
2360 | '#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de',
2361 | '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#c23531',
2362 | '#2f4554', '#61a0a8', '#d48265', '#749f83', '#ca8622',
2363 | '#bda29a', '#6e7074', '#546570', '#c4ccd3', '#f05b72'
2364 | ];
2365 |
2366 | // Adiciona variação às cores base
2367 | const color = baseColors[Math.floor(Math.random() * baseColors.length)];
2368 | const variation = Math.random() * 40 - 20; // Variação de -20 a +20
2369 |
2370 | // Converte cor para RGB
2371 | const r = parseInt(color.slice(1, 3), 16);
2372 | const g = parseInt(color.slice(3, 5), 16);
2373 | const b = parseInt(color.slice(5, 7), 16);
2374 |
2375 | // Aplica variação mantendo os valores entre 0 e 255
2376 | const newR = Math.min(255, Math.max(0, r + variation));
2377 | const newG = Math.min(255, Math.max(0, g + variation));
2378 | const newB = Math.min(255, Math.max(0, b + variation));
2379 |
2380 | // Converte de volta para hexadecimal
2381 | return '#' +
2382 | Math.round(newR).toString(16).padStart(2, '0') +
2383 | Math.round(newG).toString(16).padStart(2, '0') +
2384 | Math.round(newB).toString(16).padStart(2, '0');
2385 | }
2386 |
2387 | _processDataForHierarchy(fields) {
2388 | const hierarchy = {
2389 | name: 'Metrics',
2390 | children: []
2391 | };
2392 |
2393 | // Group data by host
2394 | const hostGroups = new Map();
2395 |
2396 | fields.forEach(field => {
2397 | const parts = field.name.split('/');
2398 | const hostName = field.host || 'Unknown Host';
2399 | const value = parseFloat(field.value);
2400 |
2401 | if (!hostGroups.has(hostName)) {
2402 | hostGroups.set(hostName, {
2403 | name: hostName,
2404 | children: []
2405 | });
2406 | }
2407 |
2408 | const hostGroup = hostGroups.get(hostName);
2409 | hostGroup.children.push({
2410 | name: parts[parts.length - 1],
2411 | value: value
2412 | });
2413 | });
2414 |
2415 | // Add host groups to hierarchy
2416 | hierarchy.children = Array.from(hostGroups.values());
2417 |
2418 | return hierarchy.children;
2419 | }
2420 |
2421 | onResize() {
2422 | super.onResize();
2423 |
2424 | if (this._state === WIDGET_STATE_ACTIVE) {
2425 | this._resizeChart();
2426 | }
2427 | }
2428 |
2429 | _resizeChart() {
2430 | if (this._chart) {
2431 | this._chart.resize();
2432 | }
2433 | }
2434 |
2435 | _getResourceColor(value) {
2436 | if (value >= 80) return '#ee6666'; // Vermelho para alto uso
2437 | if (value >= 60) return '#fac858'; // Amarelo para médio uso
2438 | return '#91cc75'; // Verde para baixo uso
2439 | }
2440 |
2441 | _getGraphHistory() {
2442 | const history = [];
2443 | const meta = this._items_meta;
2444 | const data = this._items_data;
2445 |
2446 |
2447 |
2448 | if (!data || !meta) {
2449 | console.error("Dados ou metadados ausentes");
2450 | return history;
2451 | }
2452 |
2453 | // Processando cada itemid presente nos dados
2454 | for (const itemid in data) {
2455 | if (!data.hasOwnProperty(itemid) || !meta.hasOwnProperty(itemid)) {
2456 | continue;
2457 | }
2458 |
2459 | // Obter informações do item atual
2460 | const item_data = data[itemid];
2461 | const item_meta = meta[itemid];
2462 |
2463 |
2464 |
2465 | // Obter o último valor nos dados do item
2466 | let value = null;
2467 | let clock = null;
2468 |
2469 | if (item_data.length > 0) {
2470 | const last_point = item_data[item_data.length - 1];
2471 | value = last_point.value;
2472 | clock = last_point.clock;
2473 |
2474 | // Verificar se o valor está em notação científica e convertê-lo
2475 | if (typeof value === 'string' && value.match(/^\d+\.\d+e[+-]\d+/)) {
2476 |
2477 | // Extrair a parte numérica
2478 | const matches = value.match(/^(\d+\.\d+e[+-]\d+)/);
2479 | if (matches && matches[1]) {
2480 | const numVal = Number(matches[1]);
2481 | if (!isNaN(numVal)) {
2482 | value = numVal;
2483 | }
2484 | }
2485 | }
2486 | }
2487 |
2488 | // Adicionar o item ao histórico com suas informações
2489 | if (value !== null && clock !== null) {
2490 | history.push({
2491 | itemid: itemid,
2492 | name: item_meta.name,
2493 | units: item_meta.units || '',
2494 | color: item_meta.color || this._defaultColors(history.length),
2495 | value: value,
2496 | clock: clock
2497 | });
2498 | }
2499 | }
2500 |
2501 | // Ordenar o histórico pela ordem original dos itens
2502 | history.sort((a, b) => {
2503 | return meta[a.itemid].order - meta[b.itemid].order;
2504 | });
2505 |
2506 | return history;
2507 | }
2508 |
2509 | _prepareGraphData(history) {
2510 | if (!history) {
2511 | history = this._getGraphHistory();
2512 | }
2513 |
2514 | // Verifica se há dados para processar
2515 | if (!history || history.length === 0) {
2516 | console.warn("Sem dados para mostrar no gráfico");
2517 | return [];
2518 | }
2519 |
2520 | return history;
2521 | }
2522 |
2523 | updateGraph() {
2524 |
2525 | try {
2526 | // Limpar qualquer gráfico existente
2527 | if (this._echart) {
2528 | this._echart.dispose();
2529 | this._echart = null;
2530 | }
2531 |
2532 | // Obter o contêiner do gráfico
2533 | const container = this.$('[data-container="graph"]')[0];
2534 | if (!container) {
2535 | console.error("Contêiner do gráfico não encontrado");
2536 | return;
2537 | }
2538 |
2539 | // Preparar os dados para o gráfico usando nosso novo método
2540 | const history = this._getGraphHistory();
2541 | // Verificar se há dados para o gráfico
2542 | if (!history || history.length === 0) {
2543 | console.warn("Sem dados para mostrar no gráfico");
2544 | container.innerHTML = 'Sem dados disponíveis
';
2545 | return;
2546 | }
2547 |
2548 | // Verificar o tipo de gráfico selecionado
2549 | const chartType = parseInt(this._fields_values.chart_type) || 0;
2550 |
2551 | // Inicializar o gráfico conforme o tipo
2552 | this._echart = echarts.init(container);
2553 |
2554 | // Criar o gráfico conforme o tipo selecionado
2555 | switch (chartType) {
2556 | case 0: // Linha
2557 | this._createLineChart(history);
2558 | break;
2559 | case 1: // Barra
2560 | this._createBarChart(history);
2561 | break;
2562 | case 2: // Torta
2563 | this._createPieChart(history);
2564 | break;
2565 | case 3: // Gauge
2566 | this._createGaugeChart(history);
2567 | break;
2568 | case 4: // Líquido
2569 | this._createLiquidChart(history);
2570 | break;
2571 | default:
2572 | this._createLineChart(history);
2573 | }
2574 |
2575 | // Redimensionar o gráfico se necessário
2576 | window.addEventListener('resize', () => {
2577 | if (this._echart) {
2578 | this._echart.resize();
2579 | }
2580 | });
2581 | } catch (error) {
2582 | console.error("Erro ao atualizar o gráfico:", error);
2583 | }
2584 | }
2585 | }
--------------------------------------------------------------------------------
/assets/js/echarts-liquidfill.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("echarts")):"function"==typeof define&&define.amd?define(["echarts"],t):"object"==typeof exports?exports["echarts-liquidfill"]=t(require("echarts")):e["echarts-liquidfill"]=t(e.echarts)}(self,(function(e){return(()=>{"use strict";var t={245:(e,t,a)=>{a.r(t);var i=a(83);i.extendSeriesModel({type:"series.liquidFill",optionUpdated:function(){var e=this.option;e.gridSize=Math.max(Math.floor(e.gridSize),4)},getInitialData:function(e,t){var a=i.helper.createDimensions(e.data,{coordDimensions:["value"]}),r=new i.List(a,this);return r.initData(e.data),r},defaultOption:{color:["#294D99","#156ACF","#1598ED","#45BDFF"],center:["50%","50%"],radius:"50%",amplitude:"8%",waveLength:"80%",phase:"auto",period:"auto",direction:"right",shape:"circle",waveAnimation:!0,animationEasing:"linear",animationEasingUpdate:"linear",animationDuration:2e3,animationDurationUpdate:1e3,outline:{show:!0,borderDistance:8,itemStyle:{color:"none",borderColor:"#294D99",borderWidth:8,shadowBlur:20,shadowColor:"rgba(0, 0, 0, 0.25)"}},backgroundStyle:{color:"#E3F7FF"},itemStyle:{opacity:.95,shadowBlur:50,shadowColor:"rgba(0, 0, 0, 0.4)"},label:{show:!0,color:"#294D99",insideColor:"#fff",fontSize:50,fontWeight:"bold",align:"center",baseline:"middle",position:"inside"},emphasis:{itemStyle:{opacity:.8}}}});const r=i.graphic.extendShape({type:"ec-liquid-fill",shape:{waveLength:0,radius:0,radiusY:0,cx:0,cy:0,waterLevel:0,amplitude:0,phase:0,inverse:!1},buildPath:function(e,t){null==t.radiusY&&(t.radiusY=t.radius);for(var a=Math.max(2*Math.ceil(2*t.radius/t.waveLength*4),8);t.phase<2*-Math.PI;)t.phase+=2*Math.PI;for(;t.phase>0;)t.phase-=2*Math.PI;var i=t.phase/Math.PI/2*t.waveLength,r=t.cx-t.radius+i-2*t.radius;e.moveTo(r,t.waterLevel);for(var l=0,o=0;ol?(l*=2*e/n,n=2*e):(n*=2*e/l,l=2*e);var s=t?0:M-n/2,h=t?0:P-l/2;return a=i.graphic.makePath(S.slice(7),{},new i.graphic.BoundingRect(s,h,n,l)),t&&(a.x=-n/2,a.y=-l/2),a}if(I){var d=t?-e[0]:M-e[0],p=t?-e[1]:P-e[1];return i.helper.createSymbol("rect",d,p,2*e[0],2*e[1])}return d=t?-e:M-e,p=t?-e:P-e,"pin"===S?p+=e:"arrow"===S&&(p-=e),i.helper.createSymbol(S,d,p,2*e,2*e)}return new i.graphic.Circle({shape:{cx:t?0:M,cy:t?0:P,r:e}})}function Y(){var t=E(w);return t.style.fill=null,t.setStyle(e.getModel("outline.itemStyle").getItemStyle()),t}function k(t,a,n){var o=I?u[0]:u,s=I?g/2:u,d=h.getItemModel(t),p=d.getModel("itemStyle"),c=d.get("phase"),v=l(d.get("amplitude"),2*s),f=l(d.get("waveLength"),2*o),y=s-h.get("value",t)*s*2;c=n?n.shape.phase:"auto"===c?t*Math.PI/4:c;var m=p.getItemStyle();if(!m.fill){var w=e.get("color"),b=t%w.length;m.fill=w[b]}var x=new r({shape:{waveLength:f,radius:o,radiusY:s,cx:2*o,cy:0,waterLevel:y,amplitude:v,phase:c,inverse:a},style:m,x:M,y:P});x.shape._waterLevel=y;var S=d.getModel("emphasis.itemStyle").getItemStyle();S.lineWidth=0,x.ensureState("emphasis").style=S,i.helper.enableHoverEmphasis(x);var L=E(u,!0);return L.setStyle({fill:"white"}),x.setClipPath(L),x}function q(e,t,a){var i=h.getItemModel(e),r=i.get("period"),n=i.get("direction"),l=h.get("value",e),o=i.get("phase");o=a?a.shape.phase:"auto"===o?e*Math.PI/4:o;var s,d;s="auto"===r?0===(d=h.count())?5e3:5e3*(.2+(d-e)/d*.8):"function"==typeof r?r(l,e):r;var p=0;"right"===n||null==n?p=Math.PI:"left"===n?p=-Math.PI:"none"===n?p=0:console.error("Illegal direction value for liquid fill."),"none"!==n&&i.get("waveAnimation")&&t.animate("shape",!0).when(0,{phase:o}).when(s/2,{phase:p+o}).when(s,{phase:2*p+o}).during((function(){T&&T.dirty(!0)})).start()}h.diff(D).add((function(t){var a=k(t,!1),r=a.shape.waterLevel;a.shape.waterLevel=I?g/2:u,i.graphic.initProps(a,{shape:{waterLevel:r}},e),a.z2=2,q(t,a,null),s.add(a),h.setItemGraphicEl(t,a),F.push(a)})).update((function(t,a){for(var r=D.getItemGraphicEl(a),l=k(t,!1,r),d={},p=["amplitude","cx","cy","phase","radius","radiusY","waterLevel","waveLength"],u=0;u{t.exports=e}},a={};function i(e){if(a[e])return a[e].exports;var r=a[e]={exports:{}};return t[e](r,r.exports,i),r.exports}return i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i(245)})()}));
2 | //# sourceMappingURL=echarts-liquidfill.min.js.map
--------------------------------------------------------------------------------
/includes/WidgetForm.php:
--------------------------------------------------------------------------------
1 | addField(
54 | new CWidgetFieldMultiSelectGroup('groupids', _('Host groups'))
55 | )
56 | ->addField(
57 | new CWidgetFieldMultiSelectHost('hostids', _('Hosts'))
58 | );
59 |
60 | // Adicionar o campo override_hostid para quando o widget estiver em um dashboard de template
61 | if ($this->isTemplateDashboard()) {
62 | $this->addField(
63 | (new CWidgetFieldMultiSelectOverrideHost('override_hostid', _('Host')))
64 | );
65 | }
66 |
67 | $this->addField(
68 | (new CWidgetFieldPatternSelectItem('items', _('Item patterns')))
69 | ->setFlags(CWidgetField::FLAG_LABEL_ASTERISK)
70 | )
71 | ->addField(
72 | (new CWidgetFieldSelect('display_type', _('Chart Type'), [
73 | self::DISPLAY_TYPE_GAUGE => _('Gauge Chart'),
74 | self::DISPLAY_TYPE_LIQUID => _('Liquid Chart'),
75 | self::DISPLAY_TYPE_PIE => _('Pie Chart'),
76 | self::DISPLAY_TYPE_HBAR => _('Horizontal Bar Chart'),
77 | self::DISPLAY_TYPE_MULTI_GAUGE => _('Multi-level Gauge'),
78 | self::DISPLAY_TYPE_TREEMAP => _('Treemap Chart'),
79 | self::DISPLAY_TYPE_ROSE => _('Nightingale Rose Chart'),
80 | self::DISPLAY_TYPE_FUNNEL => _('Funnel Chart'),
81 | self::DISPLAY_TYPE_TREEMAP_SUNBURST => _('Treemap/Sunburst Chart'),
82 | self::DISPLAY_TYPE_LLD_TABLE => _('LLD Table')
83 | ]))
84 | ->setDefault(self::DISPLAY_TYPE_GAUGE)
85 | ->setFlags(CWidgetField::FLAG_NOT_EMPTY)
86 | )
87 | ->addField(
88 | (new CWidgetFieldSelect('color_theme', _('Color Theme'), [
89 | self::COLOR_THEME_DEFAULT => _('Default'),
90 | self::COLOR_THEME_ZABBIX => _('Zabbix'),
91 | self::COLOR_THEME_PASTEL => _('Pastel'),
92 | self::COLOR_THEME_BRIGHT => _('Bright'),
93 | self::COLOR_THEME_DARK => _('Dark'),
94 | self::COLOR_THEME_BLUE => _('Blue Monochrome')
95 | ]))
96 | ->setDefault(self::COLOR_THEME_DEFAULT)
97 | ->setFlags(CWidgetField::FLAG_NOT_EMPTY)
98 | );
99 |
100 | return $this;
101 | }
102 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2.0,
3 | "id": "echarts",
4 | "type": "widget",
5 | "name": "ECharts Widget",
6 | "namespace": "EchartsWidget",
7 | "version": "1.3",
8 | "author": "Monzphere.com",
9 | "url": "monzphere.com",
10 | "actions": {
11 | "widget.echarts.view": {
12 | "class": "WidgetView"
13 | }
14 | },
15 | "widget": {
16 | "js_class": "WidgetEcharts"
17 | },
18 | "assets": {
19 | "css": ["widget.css"],
20 | "js": ["echarts.min.js", "echarts-liquidfill.min.js", "class.widget.js"]
21 | }
22 | }
--------------------------------------------------------------------------------
/views/widget.edit.php:
--------------------------------------------------------------------------------
1 | .
14 | **/
15 |
16 | /**
17 | * ECharts widget form view.
18 | *
19 | * @var CView $this
20 | * @var array $data
21 | */
22 |
23 | use Modules\EchartsWidget\Includes\WidgetForm;
24 |
25 | $form = new CWidgetFormView($data);
26 |
27 | $groupids_field = array_key_exists('groupids', $data['fields'])
28 | ? new CWidgetFieldMultiSelectGroupView($data['fields']['groupids'])
29 | : null;
30 |
31 | $hostids_field = $data['templateid'] === null && array_key_exists('hostids', $data['fields'])
32 | ? (new CWidgetFieldMultiSelectHostView($data['fields']['hostids']))
33 | ->setFilterPreselect([
34 | 'id' => $groupids_field->getId(),
35 | 'accept' => CMultiSelect::FILTER_PRESELECT_ACCEPT_ID,
36 | 'submit_as' => 'groupid'
37 | ])
38 | : null;
39 |
40 | // Adicionar campo override_hostid para dashboard de template
41 | $override_hostid_field = $data['templateid'] !== null && array_key_exists('override_hostid', $data['fields'])
42 | ? new CWidgetFieldMultiSelectOverrideHostView($data['fields']['override_hostid'])
43 | : null;
44 |
45 | $form
46 | ->addField($groupids_field)
47 | ->addField($hostids_field)
48 | ->addField($override_hostid_field);
49 |
50 | // Adiciona os campos na ordem correta
51 | $display_type_field = null;
52 | if (array_key_exists('display_type', $data['fields'])) {
53 | $display_type_field = new CWidgetFieldSelectView($data['fields']['display_type']);
54 | $form->addField($display_type_field);
55 | }
56 |
57 | // Adiciona o campo de tema de cores
58 | if (array_key_exists('color_theme', $data['fields'])) {
59 | $form->addField(
60 | new CWidgetFieldSelectView($data['fields']['color_theme'])
61 | );
62 | }
63 |
64 | // Adiciona o campo de items
65 | if (array_key_exists('items', $data['fields'])) {
66 | $items_field = new CWidgetFieldPatternSelectItemView($data['fields']['items']);
67 |
68 | if ($data['templateid'] === null) {
69 | // Para dashboard normal, filtra por host selecionado
70 | $items_field->setFilterPreselect($hostids_field !== null
71 | ? [
72 | 'id' => $hostids_field->getId(),
73 | 'accept' => CMultiSelect::FILTER_PRESELECT_ACCEPT_ID,
74 | 'submit_as' => 'hostid'
75 | ]
76 | : []
77 | );
78 | }
79 | else if ($override_hostid_field !== null) {
80 | // Para dashboard de template, filtra pelo host de override
81 | $items_field->setFilterPreselect([
82 | 'id' => $override_hostid_field->getId(),
83 | 'accept' => CMultiSelect::FILTER_PRESELECT_ACCEPT_ID,
84 | 'submit_as' => 'hostid'
85 | ]);
86 | }
87 |
88 | $items_field->addClass('js-item-pattern-field');
89 | $form->addField($items_field);
90 | }
91 |
92 | if (array_key_exists('unit_type', $data['fields'])) {
93 | $form->addField(
94 | new CWidgetFieldSelectView($data['fields']['unit_type'])
95 | );
96 | }
97 | $form->show();
98 |
--------------------------------------------------------------------------------
/views/widget.view.php:
--------------------------------------------------------------------------------
1 | addItem(
31 | (new CDiv())->addClass('chart')
32 | )
33 | ->setVar('items_data', $data['items_data'])
34 | ->setVar('items_meta', $data['items_meta'])
35 | ->setVar('fields_values', $data['fields_values'])
36 | ->show();
--------------------------------------------------------------------------------