diff --git a/src/css/worldmap-panel.css b/src/css/worldmap-panel.css index 94cab8b..70ebaf5 100644 --- a/src/css/worldmap-panel.css +++ b/src/css/worldmap-panel.css @@ -6,8 +6,8 @@ padding: 6px 8px; font: 14px/16px Arial, Helvetica, sans-serif; background: white; - background: rgba(255,255,255,0.8); - box-shadow: 0 0 15px rgba(0,0,0,0.2); + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); border-radius: 5px; } .info h4 { @@ -33,10 +33,58 @@ opacity: 0.7; } -.leaflet-top.leaflet-left, .leaflet-bottom.leaflet-left, .leaflet-bottom.leaflet-right { +.leaflet-top.leaflet-left, +.leaflet-bottom.leaflet-left, +.leaflet-bottom.leaflet-right { z-index: 1000; } -.leaflet-popup-content-wrapper, .leaflet-popup-tip { +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { color: #d8d9da !important; } + +.margin-top-8 { + margin-top: 8px; +} +.tooltip-container { + max-height: 100px; + overflow: auto; + color: #666666; +} + +.info.aggregations.leaflet-control { + color: #666666; + font-size: 11px; + width: 100%; +} +.agg-container { + max-height: 90px; + overflow: auto; +} +.agg-item { + display: flex; + justify-content: space-between; + align-items: center; +} +.agg-item > div:first-child { + min-width: 70px; + display: flex; +} +.agg-label { + width: 40px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-right: 2px; +} +.agg-bar { + flex: 1; + margin-left: 5px; + margin-bottom: 2px; +} + +.agg-bar-progress { + background: #0078a8; + font-size: 0; +} diff --git a/src/css/worldmap.light.css b/src/css/worldmap.light.css index 2ead013..a97b73d 100644 --- a/src/css/worldmap.light.css +++ b/src/css/worldmap.light.css @@ -1,4 +1,3 @@ .worldmap-popup .leaflet-popup-content-wrapper, .worldmap-popup .leaflet-popup-tip { background-color: #ECECEC; - color: #000 !important; } diff --git a/src/data_formatter.ts b/src/data_formatter.ts index 13121f0..065226d 100644 --- a/src/data_formatter.ts +++ b/src/data_formatter.ts @@ -22,7 +22,12 @@ export default class DataFormatter { } if (_.isString(lastValue)) { - data.push({ key: serie.alias, value: 0, valueFormatted: lastValue, valueRounded: 0 }); + data.push({ + key: serie.alias, + value: 0, + valueFormatted: lastValue, + valueRounded: 0 + }); } else { const dataValue = { key: serie.alias, @@ -31,7 +36,7 @@ export default class DataFormatter { locationLongitude: location.longitude, value: serie.stats[this.ctrl.panel.valueName], valueFormatted: lastValue, - valueRounded: 0, + valueRounded: 0 }; if (dataValue.value > highestValue) { @@ -42,7 +47,10 @@ export default class DataFormatter { lowestValue = dataValue.value; } - dataValue.valueRounded = kbn.roundValue(dataValue.value, parseInt(this.ctrl.panel.decimals, 10) || 0); + dataValue.valueRounded = kbn.roundValue( + dataValue.value, + parseInt(this.ctrl.panel.decimals, 10) || 0 + ); data.push(dataValue); } }); @@ -61,10 +69,13 @@ export default class DataFormatter { locationLongitude: decodedGeohash.longitude, value: value, valueFormatted: value, - valueRounded: 0, + valueRounded: 0 }; - dataValue.valueRounded = kbn.roundValue(dataValue.value, this.ctrl.panel.decimals || 0); + dataValue.valueRounded = kbn.roundValue( + dataValue.value, + this.ctrl.panel.decimals || 0 + ); return dataValue; } @@ -93,7 +104,12 @@ export default class DataFormatter { : encodedGeohash; const value = row[columnNames[this.ctrl.panel.esMetric]]; - const dataValue = this.createDataValue(encodedGeohash, decodedGeohash, locationName, value); + const dataValue = this.createDataValue( + encodedGeohash, + decodedGeohash, + locationName, + value + ); if (dataValue.value > highestValue) { highestValue = dataValue.value; } @@ -117,7 +133,12 @@ export default class DataFormatter { : encodedGeohash; const value = datapoint[this.ctrl.panel.esMetric]; - const dataValue = this.createDataValue(encodedGeohash, decodedGeohash, locationName, value); + const dataValue = this.createDataValue( + encodedGeohash, + decodedGeohash, + locationName, + value + ); if (dataValue.value > highestValue) { highestValue = dataValue.value; } @@ -169,9 +190,12 @@ export default class DataFormatter { let key; let longitude; let latitude; + let metricFieldChoosen: number; + let metricFieldNaN; if (this.ctrl.panel.tableQueryOptions.queryType === 'geohash') { - const encodedGeohash = datapoint[this.ctrl.panel.tableQueryOptions.geohashField]; + const encodedGeohash = + datapoint[this.ctrl.panel.tableQueryOptions.geohashField]; const decodedGeohash = decodeGeoHash(encodedGeohash); latitude = decodedGeohash.latitude; @@ -179,20 +203,40 @@ export default class DataFormatter { key = encodedGeohash; } else { latitude = datapoint[this.ctrl.panel.tableQueryOptions.latitudeField]; - longitude = datapoint[this.ctrl.panel.tableQueryOptions.longitudeField]; + longitude = + datapoint[this.ctrl.panel.tableQueryOptions.longitudeField]; key = `${latitude}_${longitude}`; } - + let locationName; + if (this.ctrl.panel.tableQueryOptions.metricField === 'TAS') { + locationName = (datapoint[this.ctrl.panel.tableQueryOptions.labelField] || datapoint["Name"]); + if (datapoint["State"] === "ACTIVE"){ + metricFieldChoosen = 11; + }else{ + metricFieldChoosen = -1; + } + metricFieldNaN = true; + } else { + metricFieldChoosen = + datapoint[this.ctrl.panel.tableQueryOptions.metricField]; + metricFieldNaN = false; + locationName = (datapoint[this.ctrl.panel.tableQueryOptions.labelField] || "n/a"); + } const dataValue = { key: key, - locationName: datapoint[this.ctrl.panel.tableQueryOptions.labelField] || 'n/a', + locationName: locationName, locationLatitude: latitude, locationLongitude: longitude, - value: datapoint[this.ctrl.panel.tableQueryOptions.metricField], - valueFormatted: datapoint[this.ctrl.panel.tableQueryOptions.metricField], + value: metricFieldChoosen || 0, + valueFormatted: + datapoint[this.ctrl.panel.tableQueryOptions.metricField], valueRounded: 0, + isMetricFieldNaN: metricFieldNaN }; - + if (this.ctrl.panel.aggregationLegendField !== '') { + dataValue[`agg-${this.ctrl.panel.aggregationLegendField}`] = + datapoint[this.ctrl.panel.aggregationLegendField]; + } if (dataValue.value > highestValue) { highestValue = dataValue.value; } @@ -201,10 +245,38 @@ export default class DataFormatter { lowestValue = dataValue.value; } - dataValue.valueRounded = kbn.roundValue(dataValue.value, this.ctrl.panel.decimals || 0); - data.push(dataValue); + dataValue.valueRounded = kbn.roundValue( + dataValue.value, + this.ctrl.panel.decimals || 0 + ); + if (latitude && longitude) { + data.push(dataValue); + } }); - + // Getting the List of columns from the table row + data.columns = Object.keys(tableData[0][0] || []); + // Aggregations + if ( + (this.ctrl.panel.aggregationLegendField !== '' && + data.columns.indexOf(this.ctrl.panel.aggregationLegendField) > -1) || + !tableData[0][0] + ) { + data.aggregations = _.countBy( + data, + `agg-${this.ctrl.panel.aggregationLegendField}` + ); + data.aggregations.unknown = + data.aggregations.Undefined || 0 + data.aggregations[''] || 0; + delete data.aggregations.undefined; + delete data.aggregations['']; + data.aggregationSortedList = Object.keys(data.aggregations).sort( + function(a, b) { + return data.aggregations[b] - data.aggregations[a]; + } + ); + } else { + data.aggregations = {}; + } data.highestValue = highestValue; data.lowestValue = lowestValue; data.valueRange = highestValue - lowestValue; @@ -223,7 +295,7 @@ export default class DataFormatter { locationLatitude: point.latitude, locationLongitude: point.longitude, value: point.value !== undefined ? point.value : 1, - valueRounded: 0, + valueRounded: 0 }; if (dataValue.value > highestValue) { highestValue = dataValue.value; diff --git a/src/partials/editor.html b/src/partials/editor.html index 63c474e..6720596 100644 --- a/src/partials/editor.html +++ b/src/partials/editor.html @@ -1,50 +1,119 @@ -<div class="editor-row"> +<!--//Broadcom has made changes to this file --> + +<div class="editor-row" xmlns="http://www.w3.org/1999/html"> <div class="section gf-form-group"> <h5 class="section-heading">Map Visual Options</h5> <div class="gf-form"> <label class="gf-form-label width-10">Center</label> <div class="gf-form-select-wrapper max-width-10"> - <select class="input-small gf-form-input" ng-model="ctrl.panel.mapCenter" ng-options="t for t in ['(0°, 0°)', 'North America', 'Europe', 'West Asia', 'SE Asia', 'custom', 'Last GeoHash']" - ng-change="ctrl.setNewMapCenter()"></select> + <select + class="input-small gf-form-input" + ng-model="ctrl.panel.mapCenter" + ng-options="t for t in ['(0°, 0°)', 'North America', 'Europe', 'West Asia', 'SE Asia', 'custom', 'Last GeoHash']" + ng-change="ctrl.setNewMapCenter()" + ></select> </div> <div class="gf-form" ng-show="ctrl.panel.mapCenter === 'custom'"> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.mapCenterLatitude" ng-change="ctrl.setNewMapCenter()" - ng-model-onblur /> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.mapCenterLongitude" ng-change="ctrl.setNewMapCenter()" - ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.mapCenterLatitude" + ng-change="ctrl.setNewMapCenter()" + ng-model-onblur + /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.mapCenterLongitude" + ng-change="ctrl.setNewMapCenter()" + ng-model-onblur + /> </div> </div> <div class="gf-form"> <label class="gf-form-label width-10">Initial Zoom</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.initialZoom" ng-change="ctrl.setZoom()" - placeholder="1" ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.initialZoom" + ng-change="ctrl.setZoom()" + placeholder="1" + ng-model-onblur + /> </div> <div class="gf-form"> <label class="gf-form-label width-10">Min Circle Size</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.circleMinSize" ng-change="ctrl.render()" - placeholder="2" ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.circleMinSize" + ng-change="ctrl.render()" + placeholder="2" + ng-model-onblur + /> </div> <div class="gf-form"> <label class="gf-form-label width-10">Max Circle Size</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.circleMaxSize" ng-change="ctrl.render()" - placeholder="30" ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.circleMaxSize" + ng-change="ctrl.render()" + placeholder="30" + ng-model-onblur + /> </div> - <gf-form-switch class="gf-form" label="Sticky Labels" label-class="width-10" checked="ctrl.panel.stickyLabels" on-change="ctrl.toggleStickyLabels()"> + <gf-form-switch + class="gf-form" + label="Sticky Labels" + label-class="width-10" + checked="ctrl.panel.stickyLabels" + on-change="ctrl.toggleStickyLabels()" + > </gf-form-switch> <div class="gf-form"> <label class="gf-form-label width-10">Decimals</label> - <input type="number" class="input-small gf-form-input width-10" ng-model="ctrl.panel.decimals" ng-change="ctrl.refresh()" - ng-model-onblur /> + <input + type="number" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.decimals" + ng-change="ctrl.refresh()" + ng-model-onblur + /> </div> <div class="gf-form"> <label class="gf-form-label width-10">Unit</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.unitSingular" placeholder="singular form" - ng-change="ctrl.render()" ng-model-onblur /> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.unitPlural" placeholder="plural form" - ng-change="ctrl.render()" ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.unitSingular" + placeholder="singular form" + ng-change="ctrl.render()" + ng-model-onblur + /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.unitPlural" + placeholder="plural form" + ng-change="ctrl.render()" + ng-model-onblur + /> </div> - <gf-form-switch class="gf-form" label="Show Legend" label-class="width-10" checked="ctrl.panel.showLegend" on-change="ctrl.toggleLegend()"></gf-form-switch> - <gf-form-switch class="gf-form" label="Mouse Wheel Zoom" label-class="width-10" checked="ctrl.panel.mouseWheelZoom" on-change="ctrl.toggleMouseWheelZoom()"></gf-form-switch> + <gf-form-switch + class="gf-form" + label="Show Legend" + label-class="width-10" + checked="ctrl.panel.showLegend" + on-change="ctrl.toggleLegend()" + ></gf-form-switch> + <gf-form-switch + class="gf-form" + label="Mouse Wheel Zoom" + label-class="width-10" + checked="ctrl.panel.mouseWheelZoom" + on-change="ctrl.toggleMouseWheelZoom()" + ></gf-form-switch> </div> <div class="section gf-form-group"> <h5 class="section-heading">Map Data Options</h5> @@ -52,161 +121,379 @@ <h5 class="section-heading">Map Data Options</h5> <div class="gf-form"> <label class="gf-form-label width-12">Location Data</label> <div class="gf-form-select-wrapper max-width-10"> - <select class="input-small gf-form-input" ng-model="ctrl.panel.locationData" ng-options="t for t in ['countries', 'countries_3letter', 'states', 'probes', 'geohash', 'json endpoint', 'jsonp endpoint', 'json result', 'table']" - ng-change="ctrl.changeLocationData()"></select> + <select + class="input-small gf-form-input" + ng-model="ctrl.panel.locationData" + ng-options="t for t in ['countries', 'countries_3letter', 'states', 'probes', 'geohash', 'json endpoint', 'jsonp endpoint', 'json result', 'table']" + ng-change="ctrl.changeLocationData()" + ></select> </div> </div> <div class="gf-form" ng-show="ctrl.panel.locationData !== 'geohash'"> <label class="gf-form-label width-12">Aggregation</label> <div class="gf-form-select-wrapper max-width-10"> - <select class="input-small gf-form-input" ng-model="ctrl.panel.valueName" ng-options="f for f in ['min','max','avg', 'current', 'total']" - ng-change="ctrl.refresh()"></select> + <select + class="input-small gf-form-input" + ng-model="ctrl.panel.valueName" + ng-options="f for f in ['min','max','avg', 'current', 'total']" + ng-change="ctrl.refresh()" + ></select> </div> </div> - <div class="gf-form" ng-show="ctrl.panel.locationData === 'json endpoint'"> + <div + class="gf-form" + ng-show="ctrl.panel.locationData === 'json endpoint'" + > <label class="gf-form-label width-12">Endpoint url</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.jsonUrl" ng-change="ctrl.refresh()" ng-model-onblur + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.jsonUrl" + ng-change="ctrl.refresh()" + ng-model-onblur /> </div> - <div class="gf-form" ng-show="ctrl.panel.locationData === 'jsonp endpoint'"> + <div + class="gf-form" + ng-show="ctrl.panel.locationData === 'jsonp endpoint'" + > <label class="gf-form-label width-12">Endpoint url</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.jsonpUrl" ng-change="ctrl.refresh()" ng-model-onblur + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.jsonpUrl" + ng-change="ctrl.refresh()" + ng-model-onblur /> </div> - <div class="gf-form" ng-show="ctrl.panel.locationData === 'jsonp endpoint'"> + <div + class="gf-form" + ng-show="ctrl.panel.locationData === 'jsonp endpoint'" + > <label class="gf-form-label width-12">Jsonp Callback</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.jsonpCallback" ng-change="ctrl.refresh()" - ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.jsonpCallback" + ng-change="ctrl.refresh()" + ng-model-onblur + /> </div> </div> - <div class="grafana-info-box max-width-28" ng-show="ctrl.panel.locationData ==='countries'"> + <div + class="grafana-info-box max-width-28" + ng-show="ctrl.panel.locationData ==='countries'" + > <h5>Mapping Between Time Series Query and Worldmap</h5> - <p>The query should be formatted as Time Series data. The time series (group by) name should be a 2-letter country code. - For example: US for United States or FR for France.</p> + <p> + The query should be formatted as Time Series data. The time series + (group by) name should be a 2-letter country code. For example: US for + United States or FR for France. + </p> </div> - <div class="grafana-info-box max-width-28" ng-show="ctrl.panel.locationData ==='countries_3letter'"> + <div + class="grafana-info-box max-width-28" + ng-show="ctrl.panel.locationData ==='countries_3letter'" + > <h5>Mapping Between Time Series Query and Worldmap</h5> - <p>The query should be formatted as Time Series data. The time series (group by) name should be a 3-letter country code. - For example: USA for United States or FRA for France.</p> + <p> + The query should be formatted as Time Series data. The time series + (group by) name should be a 3-letter country code. For example: USA for + United States or FRA for France. + </p> </div> - <div class="grafana-info-box max-width-28" ng-show="ctrl.panel.locationData ==='states'"> - <h5>Mapping Between Time Series Query and Worldmap</h5> - <p>The query should be formatted as Time Series data. The time series (group by) name should be a 2-letter US state code. - For example: CA for California.</p> - </div> - <div class="grafana-info-box max-width-28" ng-show="ctrl.panel.locationData ==='geohash'"> + <div + class="grafana-info-box max-width-28" + ng-show="ctrl.panel.locationData ==='states'" + > + <h5>Mapping Between Time Series Query and Worldmap</h5> + <p> + The query should be formatted as Time Series data. The time series + (group by) name should be a 2-letter US state code. For example: CA for + California. + </p> + </div> + <div + class="grafana-info-box max-width-28" + ng-show="ctrl.panel.locationData ==='geohash'" + > <h5>Mapping Between Geohash Query and Worldmap</h5> - <p>The query should be an Elasticsearch using the Geo Hash Grid feature or a Prometheus query that returns geohashes.</p> + <p> + The query should be an Elasticsearch using the Geo Hash Grid feature or + a Prometheus query that returns geohashes. + </p> <ul> <li> - <b>Location Name Field (optional)</b>: enter the name of the Location Name column. Used to label each circle on the - map. If it is empty then the geohash value is used as the label.</li> + <b>Location Name Field (optional)</b>: enter the name of the Location + Name column. Used to label each circle on the map. If it is empty then + the geohash value is used as the label. + </li> <li> - <b>geo_point/geohash Field</b>: enter the name of the geo_point/geohash column. This is used to calculate where the - circle should be drawn.</li> + <b>geo_point/geohash Field</b>: enter the name of the + geo_point/geohash column. This is used to calculate where the circle + should be drawn. + </li> <li> - <b>Metric Field</b>: enter the name of the metric column. This is used to give the circle a value - this determines - how large the circle is.</li> + <b>Metric Field</b>: enter the name of the metric column. This is used + to give the circle a value - this determines how large the circle is. + </li> </ul> </div> - <div class="grafana-info-box max-width-28" ng-show="ctrl.showTableGeohashOptions()"> + <div + class="grafana-info-box max-width-28" + ng-show="ctrl.showTableGeohashOptions()" + > <h5>Mapping Between Table Query and Worldmap</h5> - <p>The query should be formatted as Table data and have a geohash column and a numeric metric column.</p> + <p> + The query should be formatted as Table data and have a geohash column + and a numeric metric column. + </p> <ul> <li> - <b>Location Name Field (optional)</b>: enter the name of the Location Name column. Used to label each circle on the - map. If it is empty then the geohash value is used as the label.</li> + <b>Location Name Field (optional)</b>: enter the name of the Location + Name column. Used to label each circle on the map. If it is empty then + the geohash value is used as the label. + </li> <li> - <b>Geohash Field</b>: enter the name of the geohash column. This is used to calculate where the circle should be drawn.</li> + <b>Geohash Field</b>: enter the name of the geohash column. This is + used to calculate where the circle should be drawn. + </li> <li> - <b>Metric Field</b>: enter the name of the metric column. This is used to give the circle a value - this determines - how large the circle is.</li> + <b>Metric Field</b>: enter the name of the metric column. This is used + to give the circle a value - this determines how large the circle is. + </li> </ul> </div> - <div class="grafana-info-box max-width-28" ng-show="ctrl.showTableCoordinateOptions()"> + <div + class="grafana-info-box max-width-28" + ng-show="ctrl.showTableCoordinateOptions()" + > <h5>Mapping Between Table Query and Worldmap</h5> - <p>The query should be formatted as Table data and contain latitude, longitude columns and a numeric metric column.</p> + <p> + The query should be formatted as Table data and contain latitude, + longitude columns and a numeric metric column. + </p> <ul> <li> - <b>Location Name Field (optional)</b>: enter the name of the Location Name column. Used to label each circle on the - map. If it is empty then the value N/A is used as the label.</li> + <b>Location Name Field (optional)</b>: enter the name of the Location + Name column. Used to label each circle on the map. If it is empty then + the value N/A is used as the label. + </li> <li> - <b>Latitude/Longitude Fields</b>: enter the name of the latitude and longitude columns. These are used to calculate - where the circle should be drawn.</li> + <b>Latitude/Longitude Fields</b>: enter the name of the latitude and + longitude columns. These are used to calculate where the circle should + be drawn. + </li> <li> - <b>Metric Field</b>: enter the name of the metric column. This is used to give the circle a value - this determines - how large the circle is.</li> + <b>Metric Field</b>: enter the name of the metric column. This is used + to give the circle a value - this determines how large the circle is. + If the selected Datasource is AIOps_Inventory, then specify the value as TAS. + </li> </ul> </div> - <h6 ng-show="ctrl.panel.locationData === 'table' || ctrl.panel.locationData === 'geohash'">Field Mapping</h6> + <h6 + ng-show="ctrl.panel.locationData === 'table' || ctrl.panel.locationData === 'geohash'" + > + Field Mapping + </h6> <div class="gf-form" ng-show="ctrl.panel.locationData === 'table'"> <label class="gf-form-label width-12">Table Query Format</label> <div class="gf-form-select-wrapper max-width-10"> - <select class="input-small gf-form-input" ng-model="ctrl.panel.tableQueryOptions.queryType" ng-options="t for t in ['geohash', 'coordinates']" - ng-change="ctrl.refresh()"></select> + <select + class="input-small gf-form-input" + ng-model="ctrl.panel.tableQueryOptions.queryType" + ng-options="t for t in ['geohash', 'coordinates']" + ng-change="ctrl.refresh()" + ></select> </div> </div> <div class="gf-form" ng-show="ctrl.panel.locationData === 'table'"> <label class="gf-form-label width-12">Location Name Field</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.tableQueryOptions.labelField" ng-change="ctrl.refresh()" - ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.tableQueryOptions.labelField" + ng-change="ctrl.refresh()" + ng-model-onblur + /> </div> <div class="gf-form" ng-show="ctrl.panel.locationData === 'table'"> <label class="gf-form-label width-12">Metric Field</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.tableQueryOptions.metricField" ng-change="ctrl.refresh()" - ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.tableQueryOptions.metricField" + ng-change="ctrl.refresh()" + ng-model-onblur + /> </div> <div class="gf-form" ng-show="ctrl.showTableGeohashOptions()"> <label class="gf-form-label width-12">Geohash Field</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.tableQueryOptions.geohashField" ng-change="ctrl.refresh()" - ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.tableQueryOptions.geohashField" + ng-change="ctrl.refresh()" + ng-model-onblur + /> </div> <div class="gf-form" ng-show="ctrl.showTableCoordinateOptions()"> <label class="gf-form-label width-12">Latitude Field</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.tableQueryOptions.latitudeField" ng-change="ctrl.refresh()" - ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.tableQueryOptions.latitudeField" + ng-change="ctrl.refresh()" + ng-model-onblur + /> </div> <div class="gf-form" ng-show="ctrl.showTableCoordinateOptions()"> <label class="gf-form-label width-12">Longitude Field</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.tableQueryOptions.longitudeField" ng-change="ctrl.refresh()" - ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.tableQueryOptions.longitudeField" + ng-change="ctrl.refresh()" + ng-model-onblur + /> </div> <div class="gf-form" ng-show="ctrl.panel.locationData === 'geohash'"> <label class="gf-form-label width-12">Location Name Field</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.esLocationName" ng-change="ctrl.refresh()" - ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.esLocationName" + ng-change="ctrl.refresh()" + ng-model-onblur + /> </div> <div class="gf-form" ng-show="ctrl.panel.locationData === 'geohash'"> <label class="gf-form-label width-12">geo_point/geohash Field</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.esGeoPoint" ng-change="ctrl.refresh()" - ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.esGeoPoint" + ng-change="ctrl.refresh()" + ng-model-onblur + /> </div> <div class="gf-form" ng-show="ctrl.panel.locationData === 'geohash'"> <label class="gf-form-label width-12">Metric Field</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.esMetric" ng-change="ctrl.refresh()" ng-model-onblur + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.esMetric" + ng-change="ctrl.refresh()" + ng-model-onblur /> </div> + <div ng-show="ctrl.panel.locationData === 'table'"> + <div class="section gf-form-group"> + <h5 class="section-heading margin-top-8"> + Aggregations Legends + <tip + >When enabled, points that are plotted on the world map are aggregated by the selected <b> Aggregation_Field</b> and the aggregated result is displayed on the chart. + </tip> + </h5> + <gf-form-switch + class="gf-form" + label="Display Aggregations" + label-class="width-12" + checked="ctrl.panel.aggregationLegends" + on-change="ctrl.toggleAggregations()" + > + </gf-form-switch> + <div class="gf-form"> + <label class="gf-form-label width-12">Aggregation Field</label> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.aggregationLegendField" + ng-change="ctrl.refresh()" + ng-model-onblur + /> + </div> + </div> + </div> + <div ng-show="ctrl.panel.locationData === 'table'"> + <div class="section gf-form-group"> + <h5 class="section-heading margin-top-8">DrillDown</h5> + <div class="gf-form"> + <label class="gf-form-label width-12" + >Target<tip>URL of the target where the service is deployed.<br>use ${__locationName} for the target value </br></tip></label + > + <input + type="text" + class="input-small gf-form-input width-30" + ng-model="ctrl.panel.drilldownTarget" + ng-change="ctrl.refresh()" + ng-model-onblur + placeholder="https://your-grafana-server/d/id/name?var-name=${__locationName}" + /> + </div> + <gf-form-switch + class="gf-form" + label="Open in new tab" + label-class="width-12" + checked="ctrl.panel.drillDownTab" + on-change="ctrl.render()" + > + </gf-form-switch> + </div> + </div> </div> <div class="section gf-form-group"> <h5 class="section-heading">Threshold Options</h5> <div class="gf-form"> <label class="gf-form-label width-10">Thresholds</label> - <input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.thresholds" ng-change="ctrl.changeThresholds()" - placeholder="0,10" ng-model-onblur /> + <input + type="text" + class="input-small gf-form-input width-10" + ng-model="ctrl.panel.thresholds" + ng-change="ctrl.changeThresholds()" + placeholder="0,10" + ng-model-onblur + /> </div> <div class="gf-form"> <label class="gf-form-label width-10">Colors</label> - <spectrum-picker class="gf-form-input width-3" ng-repeat="color in ctrl.panel.colors track by $index" ng-model="ctrl.panel.colors[$index]" - ng-change="ctrl.changeThresholds()"></spectrum-picker> + <spectrum-picker + class="gf-form-input width-3" + ng-repeat="color in ctrl.panel.colors track by $index" + ng-model="ctrl.panel.colors[$index]" + ng-change="ctrl.changeThresholds()" + ></spectrum-picker> </div> </div> <div class="section gf-form-group"> <h5 class="section-heading">Hide series</h5> - <gf-form-switch class="gf-form" label="With only nulls" label-class="width-10" checked="ctrl.panel.hideEmpty" on-change="ctrl.render()"> + <gf-form-switch + class="gf-form" + label="With only nulls" + label-class="width-10" + checked="ctrl.panel.hideEmpty" + on-change="ctrl.render()" + > </gf-form-switch> - <gf-form-switch class="gf-form" label="With only zeros" label-class="width-10" checked="ctrl.panel.hideZero" on-change="ctrl.render()"> + <gf-form-switch + class="gf-form" + label="With only zeros" + label-class="width-10" + checked="ctrl.panel.hideZero" + on-change="ctrl.render()" + > </gf-form-switch> </div> + <!-- Regex--> + <div class="section gf-form-group"> + <h5 class="section-heading">Regex</h5> + <div class="gf-form"> + <label class="gf-form-label width-8">Regex Pattern + <tip>Optional, The values in the specified column are filtered and displayed according to this regex. Ex: String: Url|broadcom.com|mirror|location-1 regex: /Url\|(.*?)\|/ Output: broadcom.com</tip> + </label> + <input type="text" class="gf-form-input" ng-model="ctrl.panel.regexPattern" ng-blur="ctrl.render()" placeholder="regex"> + </div> + </div> </div> diff --git a/src/plugin.json b/src/plugin.json index 1afa70a..8f590d2 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -23,8 +23,8 @@ {"name": "USA", "path": "images/worldmap-usa.png"}, {"name": "Light Theme", "path": "images/worldmap-light-theme.png"} ], - "version": "1.0.1", - "updated": "Fri May 15 14:40:24 MDT 2020" + "version": "0.2.1", + "updated": "2019-08-29" }, "dependencies": { diff --git a/src/worldmap.ts b/src/worldmap.ts index 341f9a6..0e52a93 100644 --- a/src/worldmap.ts +++ b/src/worldmap.ts @@ -4,19 +4,21 @@ import WorldmapCtrl from './worldmap_ctrl'; const tileServers = { 'CartoDB Positron': { - url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', + url: + 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> ' + '© <a href="http://cartodb.com/attributions">CartoDB</a>', - subdomains: 'abcd', + subdomains: 'abcd' }, 'CartoDB Dark': { - url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', + url: + 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> ' + '© <a href="http://cartodb.com/attributions">CartoDB</a>', - subdomains: 'abcd', - }, + subdomains: 'abcd' + } }; export default class WorldMap { @@ -26,11 +28,14 @@ export default class WorldMap { map: any; legend: any; circlesLayer: any; + aggregations: any; + templateSrv: any; - constructor(ctrl, mapContainer) { + constructor(ctrl, mapContainer, templateSrv?) { this.ctrl = ctrl; this.mapContainer = mapContainer; this.circles = []; + this.templateSrv = templateSrv; } createMap() { @@ -42,7 +47,7 @@ export default class WorldMap { worldCopyJump: true, preferCanvas: true, center: mapCenter, - zoom: parseInt(this.ctrl.panel.initialZoom, 10) || 1, + zoom: parseInt(this.ctrl.panel.initialZoom, 10) || 1 }); this.setMouseWheelZoom(); @@ -52,7 +57,7 @@ export default class WorldMap { subdomains: selectedTileServer.subdomains, reuseTiles: true, detectRetina: true, - attribution: selectedTileServer.attribution, + attribution: selectedTileServer.attribution }).addTo(this.map); } @@ -80,13 +85,63 @@ export default class WorldMap { this.ctrl.panel.colors[index + 1] + '"></i> ' + thresholds[index] + - (thresholds[index + 1] ? '–' + thresholds[index + 1] + '</div>' : '+'); + (thresholds[index + 1] + ? '–' + thresholds[index + 1] + '</div>' + : '+'); } this.legend._div.innerHTML = legendHtml; }; this.legend.addTo(this.map); } + createAggregations() { + this.aggregations = (<any>window).L.control({ position: 'bottomright' }); + this.aggregations.onAdd = () => { + this.aggregations._div = (<any>window).L.DomUtil.create( + 'div', + 'info aggregations' + ); + this.aggregations.update(); + return this.aggregations._div; + }; + + this.aggregations.update = () => { + const aggregations = this.ctrl.data.aggregations || {}; + let aggregationsHtml = ''; + if (Object.keys(aggregations).length <= 1 && aggregations.unknown === 0) { + aggregationsHtml = '<div>No Data Available</div>'; + } else if ( + this.ctrl.data.aggregationSortedList && + this.ctrl.data.aggregationSortedList.length && + Object.keys(aggregations).length > 1 + ) { + this.ctrl.data.aggregationSortedList.forEach(agg => { + if (aggregations[agg] !== 0) { + aggregationsHtml = `${aggregationsHtml} + <div class="agg-item"> + <div> + <div class="agg-label" title=${agg}>${agg}</div> + <b>${(100 * (aggregations[agg] / this.ctrl.data.length)) + .toFixed(2) + .replace(/[.,]00$/, '')}%</b> + </div> + <div class="agg-bar"> + <div class="agg-bar-progress" style="width: ${100 * + (aggregations[agg] / this.ctrl.data.length)}%"> + progress + </div> + </div> + </div>`; + } + }); + } else { + aggregationsHtml = `Invalid mapping for aggregations`; + } + this.aggregations._div.innerHTML = `<div class="agg-container">${aggregationsHtml}</div>`; + }; + this.aggregations.addTo(this.map); + } + needToRedrawCircles(data) { if (this.circles.length === 0 && data.length > 0) { return true; @@ -103,7 +158,10 @@ export default class WorldMap { filterEmptyAndZeroValues(data) { return _.filter(data, o => { - return !(this.ctrl.panel.hideEmpty && _.isNil(o.value)) && !(this.ctrl.panel.hideZero && o.value === 0); + return ( + !(this.ctrl.panel.hideEmpty && _.isNil(o.value)) && + !(this.ctrl.panel.hideZero && o.value === 0) + ); }); } @@ -117,11 +175,17 @@ export default class WorldMap { drawCircles() { const data = this.filterEmptyAndZeroValues(this.ctrl.data); - if (this.needToRedrawCircles(data)) { + this.clearCircles(); + this.createCircles(data); + /*if (this.needToRedrawCircles(data)) { this.clearCircles(); this.createCircles(data); } else { this.updateCircles(data); + }*/ + // Update Aggregations + if (this.aggregations && this.aggregations !== null) { + this.aggregations.update(); } } @@ -131,7 +195,7 @@ export default class WorldMap { if (!dataPoint.locationName) { return; } - circles.push(this.createCircle(dataPoint)); + circles.push(this.createCircle(dataPoint, data)); }); this.circlesLayer = this.addCircles(circles); this.circles = circles; @@ -148,66 +212,120 @@ export default class WorldMap { }); if (circle) { + circle.options.dataset = data; circle.setRadius(this.calcCircleSize(dataPoint.value || 0)); circle.setStyle({ color: this.getColor(dataPoint.value), fillColor: this.getColor(dataPoint.value), fillOpacity: 0.5, - location: dataPoint.key, + location: dataPoint.key }); circle.unbindPopup(); - this.createPopup(circle, dataPoint.locationName, dataPoint.valueRounded); + this.createPopup( + circle, + dataPoint.locationName, + dataPoint.valueRounded, + dataPoint.isMetricFieldNaN + ); } }); + if (this.aggregations && this.aggregations !== null) { + this.aggregations.update(); + } } - createCircle(dataPoint) { - const circle = (<any>window).L.circleMarker([dataPoint.locationLatitude, dataPoint.locationLongitude], { - radius: this.calcCircleSize(dataPoint.value || 0), - color: this.getColor(dataPoint.value), - fillColor: this.getColor(dataPoint.value), - fillOpacity: 0.5, - location: dataPoint.key, - }); - - this.createPopup(circle, dataPoint.locationName, dataPoint.valueRounded); + createCircle(dataPoint, dataset?) { + const circle = (<any>window).L.circleMarker( + [dataPoint.locationLatitude, dataPoint.locationLongitude], + { + radius: this.calcCircleSize(dataPoint.value || 0), + color: this.getColor(dataPoint.value), + fillColor: this.getColor(dataPoint.value), + fillOpacity: 0.5, + location: dataPoint.key, + dataset + } + ); + this.createPopup( + circle, + dataPoint.locationName, + dataPoint.valueRounded, + dataPoint.isMetricFieldNaN + ); return circle; } calcCircleSize(dataPointValue) { - const circleMinSize = parseInt(this.ctrl.panel.circleMinSize, 10) || 2; - const circleMaxSize = parseInt(this.ctrl.panel.circleMaxSize, 10) || 30; + const circleMinSize = parseInt(this.ctrl.panel.circleMinSize, 10) || 10; + const circleMaxSize = parseInt(this.ctrl.panel.circleMaxSize, 10) || 10; if (this.ctrl.data.valueRange === 0) { return circleMaxSize; } - const dataFactor = (dataPointValue - this.ctrl.data.lowestValue) / this.ctrl.data.valueRange; + const dataFactor = + (dataPointValue - this.ctrl.data.lowestValue) / this.ctrl.data.valueRange; const circleSizeRange = circleMaxSize - circleMinSize; return circleSizeRange * dataFactor + circleMinSize; } - createPopup(circle, locationName, value) { - const unit = value && value === 1 ? this.ctrl.panel.unitSingular : this.ctrl.panel.unitPlural; - const label = (locationName + ': ' + value + ' ' + (unit || '')).trim(); - circle.bindPopup(label, { - offset: (<any>window).L.point(0, -2), - className: 'worldmap-popup', - closeButton: this.ctrl.panel.stickyLabels, - }); + createPopup(circle, locationName, value, isMetricFieldNaN) { + const unit = + value && value === 1 + ? this.ctrl.panel.unitSingular + : this.ctrl.panel.unitPlural; + + let formattedLocationList = ''; + circle.options.dataset + .filter(data => data.key === circle.options.location) + .forEach(location => { + if ( + this.ctrl.panel.drilldownTarget && + this.ctrl.panel.drilldownTarget !== '' + ) { + const url = this.ctrl.panel.drilldownTarget + ? this.ctrl.panel.drilldownTarget.split('${__locationName}').join(`${location.locationName}`) : ''; + const hrefUrl = this.templateSrv.replace(url); + + formattedLocationList = `${formattedLocationList} + <div> + ${` + ${ + this.ctrl.panel.drillDownTab + ? `<a href="${hrefUrl}" target="_blank">${location.locationName}</a> ` + : `<a href="${hrefUrl}">${location.locationName}</a>` + } + ${!isMetricFieldNaN ? `: ${location.value}` : ''} + ${unit ? `(${unit})` : ''}`.trim()} + </div>`; + } else { + formattedLocationList = `${formattedLocationList} <div> + ${`${location.locationName} ${ + !isMetricFieldNaN ? `: ${location.value}` : '' + } + ${unit ? `(${unit})` : ''}`.trim()} + </div>`; + } + const label = `<div class="tooltip-container">${formattedLocationList}</div>`; + circle.bindPopup(label, { + offset: (<any>window).L.point(0, -2), + className: 'worldmap-popup', + closeButton: this.ctrl.panel.stickyLabels + }); - circle.on('mouseover', function onMouseOver(evt) { - const layer = evt.target; - layer.bringToFront(); - this.openPopup(); - }); + circle.on('mouseover', function onMouseOver(evt) { + const layer = evt.target; + layer.bringToFront(); + this.openPopup(); + }); - if (!this.ctrl.panel.stickyLabels) { - circle.on('mouseout', function onMouseOut() { - circle.closePopup(); + if (!this.ctrl.panel.stickyLabels) { + circle.on('mouseout', function onMouseOut() { + circle.closePopup(); + }); + } }); - } } getColor(value) { @@ -224,7 +342,10 @@ export default class WorldMap { } panToMapCenter() { - this.map.panTo([parseFloat(this.ctrl.panel.mapCenterLatitude), parseFloat(this.ctrl.panel.mapCenterLongitude)]); + this.map.panTo([ + parseFloat(this.ctrl.panel.mapCenterLatitude), + parseFloat(this.ctrl.panel.mapCenterLongitude) + ]); this.ctrl.mapCenterMoved = false; } @@ -233,6 +354,11 @@ export default class WorldMap { this.legend = null; } + removeAggregations() { + this.aggregations.remove(this.map); + this.aggregations = null; + } + setMouseWheelZoom() { if (!this.ctrl.panel.mouseWheelZoom) { this.map.scrollWheelZoom.disable(); @@ -261,6 +387,9 @@ export default class WorldMap { if (this.legend) { this.removeLegend(); } + if (this.aggregations) { + this.removeAggregations(); + } this.map.remove(); } } diff --git a/src/worldmap_ctrl.ts b/src/worldmap_ctrl.ts index 4aa746c..e5fe43a 100644 --- a/src/worldmap_ctrl.ts +++ b/src/worldmap_ctrl.ts @@ -1,59 +1,61 @@ -import { MetricsPanelCtrl } from "grafana/app/plugins/sdk"; -import TimeSeries from "grafana/app/core/time_series2"; +import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; +import TimeSeries from 'grafana/app/core/time_series2'; import appEvents from 'grafana/app/core/app_events'; -import * as _ from "lodash"; -import DataFormatter from "./data_formatter"; -import "./css/worldmap-panel.css"; -import $ from "jquery"; -import "./css/leaflet.css"; -import WorldMap from "./worldmap"; +import * as _ from 'lodash'; +import DataFormatter from './data_formatter'; +import './css/worldmap-panel.css'; +import $ from 'jquery'; +import './css/leaflet.css'; +import WorldMap from './worldmap'; const panelDefaults = { maxDataPoints: 1, - mapCenter: "(0°, 0°)", + mapCenter: '(0°, 0°)', mapCenterLatitude: 0, mapCenterLongitude: 0, initialZoom: 1, - valueName: "total", - circleMinSize: 2, - circleMaxSize: 30, - locationData: "countries", - thresholds: "0,10", + valueName: 'total', + circleMinSize: 10, + circleMaxSize: 10, + locationData: 'table', + thresholds: '0,10', colors: [ - "rgba(245, 54, 54, 0.9)", - "rgba(237, 129, 40, 0.89)", - "rgba(50, 172, 45, 0.97)" + 'rgba(245, 54, 54, 0.9)', + 'rgba(237, 129, 40, 0.89)', + 'rgba(50, 172, 45, 0.97)' ], - unitSingle: "", - unitPlural: "", + unitSingle: '', + unitPlural: '', showLegend: true, mouseWheelZoom: false, - esMetric: "Count", + esMetric: 'Count', decimals: 0, hideEmpty: false, hideZero: false, stickyLabels: false, tableQueryOptions: { - queryType: "geohash", - geohashField: "geohash", - latitudeField: "latitude", - longitudeField: "longitude", - metricField: "metric" - } + queryType: 'coordinates', + geohashField: 'geohash', + latitudeField: 'latitude', + longitudeField: 'longitude', + metricField: 'metric' + }, + aggregationLegends: false, + aggregationLegendField: '' }; const mapCenters = { - "(0°, 0°)": { mapCenterLatitude: 0, mapCenterLongitude: 0 }, - "North America": { mapCenterLatitude: 40, mapCenterLongitude: -100 }, + '(0°, 0°)': { mapCenterLatitude: 0, mapCenterLongitude: 0 }, + 'North America': { mapCenterLatitude: 40, mapCenterLongitude: -100 }, Europe: { mapCenterLatitude: 46, mapCenterLongitude: 14 }, - "West Asia": { mapCenterLatitude: 26, mapCenterLongitude: 53 }, - "SE Asia": { mapCenterLatitude: 10, mapCenterLongitude: 106 }, - "Last GeoHash": { mapCenterLatitude: 0, mapCenterLongitude: 0 } + 'West Asia': { mapCenterLatitude: 26, mapCenterLongitude: 53 }, + 'SE Asia': { mapCenterLatitude: 10, mapCenterLongitude: 106 }, + 'Last GeoHash': { mapCenterLatitude: 0, mapCenterLongitude: 0 } }; export default class WorldmapCtrl extends MetricsPanelCtrl { - static templateUrl = "partials/module.html"; + static templateUrl = 'partials/module.html'; dataFormatter: DataFormatter; locations: any; @@ -73,26 +75,26 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { this.dataFormatter = new DataFormatter(this); - this.events.on("init-edit-mode", this.onInitEditMode.bind(this)); - this.events.on("data-received", this.onDataReceived.bind(this)); - this.events.on("panel-teardown", this.onPanelTeardown.bind(this)); - this.events.on("data-snapshot-load", this.onDataSnapshotLoad.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('panel-teardown', this.onPanelTeardown.bind(this)); + this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); this.loadLocationDataFromFile(); } setMapProvider(contextSrv) { this.tileServer = contextSrv.user.lightTheme - ? "CartoDB Positron" - : "CartoDB Dark"; + ? 'CartoDB Positron' + : 'CartoDB Dark'; this.setMapSaturationClass(); } setMapSaturationClass() { - if (this.tileServer === "CartoDB Dark") { - this.saturationClass = "map-darken"; + if (this.tileServer === 'CartoDB Dark') { + this.saturationClass = 'map-darken'; } else { - this.saturationClass = ""; + this.saturationClass = ''; } } @@ -106,23 +108,23 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { return; } - if (this.panel.locationData === "jsonp endpoint") { + if (this.panel.locationData === 'jsonp endpoint') { if (!this.panel.jsonpUrl || !this.panel.jsonpCallback) { return; } $.ajax({ - type: "GET", - url: this.panel.jsonpUrl + "?callback=?", - contentType: "application/json", + type: 'GET', + url: this.panel.jsonpUrl + '?callback=?', + contentType: 'application/json', jsonpCallback: this.panel.jsonpCallback, - dataType: "jsonp", + dataType: 'jsonp', success: res => { this.locations = res; this.render(); } }); - } else if (this.panel.locationData === "json endpoint") { + } else if (this.panel.locationData === 'json endpoint') { if (!this.panel.jsonUrl) { return; } @@ -131,16 +133,16 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { this.locations = res; this.render(); }); - } else if (this.panel.locationData === "table") { + } else if (this.panel.locationData === 'table') { // .. Do nothing } else if ( - this.panel.locationData !== "geohash" && - this.panel.locationData !== "json result" + this.panel.locationData !== 'geohash' && + this.panel.locationData !== 'json result' ) { $.getJSON( - "public/plugins/grafana-worldmap-panel/data/" + + 'public/plugins/grafana-worldmap-panel/data/' + this.panel.locationData + - ".json" + '.json' ).then(this.reloadLocations.bind(this)); } } @@ -152,15 +154,15 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { showTableGeohashOptions() { return ( - this.panel.locationData === "table" && - this.panel.tableQueryOptions.queryType === "geohash" + this.panel.locationData === 'table' && + this.panel.tableQueryOptions.queryType === 'geohash' ); } showTableCoordinateOptions() { return ( - this.panel.locationData === "table" && - this.panel.tableQueryOptions.queryType === "coordinates" + this.panel.locationData === 'table' && + this.panel.tableQueryOptions.queryType === 'coordinates' ); } @@ -171,9 +173,9 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } onInitEditMode() { - this.addEditorTab( - "Worldmap", - "public/plugins/grafana-worldmap-panel/partials/editor.html", + this.addEditorTab( + 'Worldmap', + 'public/plugins/grafana-worldmap-panel/partials/editor.html', 2 ); } @@ -190,12 +192,12 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { const data = []; - if (this.panel.locationData === "geohash") { + if (this.panel.locationData === 'geohash') { this.dataFormatter.setGeohashValues(dataList, data); - } else if (this.panel.locationData === "table") { + } else if (this.panel.locationData === 'table') { const tableData = dataList.map(DataFormatter.tableHandler.bind(this)); this.dataFormatter.setTableValues(tableData, data); - } else if (this.panel.locationData === "json result") { + } else if (this.panel.locationData === 'json result') { this.series = dataList; this.dataFormatter.setJsonValues(data); } else { @@ -206,13 +208,13 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { this.updateThresholdData(); - if (this.data.length && this.panel.mapCenter === "Last GeoHash") { + if (this.data.length && this.panel.mapCenter === 'Last GeoHash') { this.centerOnLastGeoHash(); } else { this.render(); } } catch (err) { - appEvents.emit('alert-error', ['Data error', err.toString()]) + appEvents.emit('alert-error', ['Data error', err.toString()]); } } @@ -239,7 +241,7 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } setNewMapCenter() { - if (this.panel.mapCenter !== "custom") { + if (this.panel.mapCenter !== 'custom') { this.panel.mapCenterLatitude = mapCenters[this.panel.mapCenter].mapCenterLatitude; this.panel.mapCenterLongitude = @@ -260,6 +262,12 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { this.render(); } + toggleAggregations() { + if (!this.panel.aggregationLegends) { + this.map.removeAggregations(); + } + this.render(); + } toggleMouseWheelZoom() { this.map.setMouseWheelZoom(); this.render(); @@ -277,7 +285,7 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } updateThresholdData() { - this.data.thresholds = this.panel.thresholds.split(",").map(strValue => { + this.data.thresholds = this.panel.thresholds.split(',').map(strValue => { return Number(strValue.trim()); }); while (_.size(this.panel.colors) > _.size(this.data.thresholds) + 1) { @@ -286,15 +294,69 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } while (_.size(this.panel.colors) < _.size(this.data.thresholds) + 1) { // not enough colors. add one. - const newColor = "rgba(50, 172, 45, 0.97)"; + const newColor = 'rgba(50, 172, 45, 0.97)'; this.panel.colors.push(newColor); } } + stringStartsAsRegEx(str: string): boolean { + if (!str) { + return false; + } + + return str[0] === '/'; + } + + stringToJsRegex(str: string): RegExp { + if (!this.stringStartsAsRegEx(str)) { + return new RegExp(`^${str}$`); + } + + const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); + + if (!match) { + throw new Error(`'${str}' is not a valid regular expression.`); + } + + return new RegExp(match[1], match[2]); + } + applyRegexPattern() { + let seriesList = this.data; + for (let i = 0; i < seriesList.length; i++) { + if (!seriesList[i].locName) { + seriesList[i].locName = seriesList[i].locationName + } + if (this.panel.regexPattern !== '' && this.panel.regexPattern !== undefined) { + const regexVal = this.stringToJsRegex(this.panel.regexPattern); + if (seriesList[i].locName && regexVal.test(seriesList[i].locName.toString())) { + const temp = regexVal.exec(seriesList[i].locName.toString()); + if (!temp) { + continue; + } + let extractedtxt = ''; + if (temp.length > 1) { + temp.slice(1).forEach((value, i) => { + if (value) { + extractedtxt += extractedtxt.length > 0 ? ' ' + value.toString() : value.toString(); + } + }); + seriesList[i].locationName = extractedtxt; + } + } + else { + seriesList[i].locationName = seriesList[i].locName; + } + } + else { + seriesList[i].locationName = seriesList[i].locName; + } + } + this.data = seriesList; + } changeLocationData() { this.loadLocationDataFromFile(true); - if (this.panel.locationData === "geohash") { + if (this.panel.locationData === 'geohash') { this.render(); } } @@ -302,12 +364,12 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { link(scope, elem, attrs, ctrl) { let firstRender = true; - ctrl.events.on("render", () => { + ctrl.events.on('render', () => { render(); ctrl.renderingCompleted(); }); - function render() { + function render() { if (!ctrl.data) { return; } @@ -319,14 +381,16 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { return; } - const mapContainer = elem.find(".mapcontainer"); + ctrl.applyRegexPattern(); + + const mapContainer = elem.find('.mapcontainer'); - if (mapContainer[0].id.indexOf("{{") > -1) { + if (mapContainer[0].id.indexOf('{{') > -1) { return; } if (!ctrl.map) { - const map = new WorldMap(ctrl, mapContainer[0]); + const map = new WorldMap(ctrl, mapContainer[0], ctrl.templateSrv); map.createMap(); ctrl.map = map; } @@ -341,6 +405,10 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { ctrl.map.createLegend(); } + if (!ctrl.map.aggregations && ctrl.panel.aggregationLegends) { + ctrl.map.createAggregations(); + } + ctrl.map.drawCircles(); } }