diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e09efb4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.4.4-custom] - 2025-08 +### Fixed +- fixed server removal issues + +### Improved +- Redesigned Refresh button as dropdown + icon combination + +### Added +- Delete Index functionality with checkbox selection +- filtering for index selector in Structured Query tab diff --git a/Readme.md b/Readme.md index 3794a32..6559b5c 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,27 @@ -# Multi Elasticsearch Head +# Multi Elasticsearch Head - Custom + +This is a fork of [Multi Elasticsearch Head](https://github.com/vorapoap/elasticsearch-head-chrome) with additional features for improved usability in multi-tenant Elasticsearch environments. + +## 🚀 New Features (Custom - 2025-08) + +### Index Filtering +- **Enhanced Index Selector**: Added filtering capability to the index selector in the Structured Query tab +- **Multi-tenant Support**: Improved usability for environments with tenant-prefixed index names + +### Local Setting + +* you can use python. +``` +cd /Users/~~/Documents/elasticsearch-head-chrome && python -m http.server 8000 +``` + +* If you need local chrome extension, then run build.py +``` +python build.py +``` + +----- +## Original Project This is a major improvement over Elasticsearch Head extension. * Add dropdown for multiple Elasticsearch Head end-points @@ -21,7 +44,7 @@ This was created because ElasticSearch 5 removed the ability to run ElasticSearc ## Installation -Head over to [Multi Elasticsearch Head](https://chrome.google.com/webstore/detail/multi-elasticsearch-head/cpmmilfkofbeimbmgiclohpodggeheim) page on the Chrome Web Store. +Head over to [Multi Elasticsearch Head](https://chrome.google.com/webstore/detail/multi-elasticsearch-head/cpmmilfkofbeimbmgiclohpodggeheim) page on the Chrome Web Store. ## Usage @@ -40,4 +63,13 @@ To make this more convenient to use (ie: without having to enter in the remote E `ssh -N -L 29200:myeshost:9200 myusername@jumphost` * then use `chrome-extension://ffmkiejjmecolpfloofpjologoblkegm/elasticsearch-head/index.html?base_uri=http://localhost:29200` to get to the remote ES cluster. +## License + +This project is licensed under the MIT License - see the original project for details. + +## Acknowledgments + +- Original project: [Multi Elasticsearch Head](https://github.com/lyfeyaj/elasticsearch-head-chrome) +- Based on: [Elasticsearch Head](https://github.com/mobz/elasticsearch-head) + diff --git a/build.py b/build.py new file mode 100644 index 0000000..a7d269a --- /dev/null +++ b/build.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import os +import shutil +import zipfile +from pathlib import Path + +print('Building Elasticsearch Head Chrome Extension(Custom)...') + +build_dir = 'build' +if os.path.exists(build_dir): + shutil.rmtree(build_dir) +os.makedirs(build_dir) + +files_to_copy = [ + 'manifest.json', + 'src/background.js', + 'elasticsearch-head/index.html', + 'elasticsearch-head/app.js', + 'elasticsearch-head/app.css', + 'elasticsearch-head/vendor.js', + 'elasticsearch-head/vendor.css', + 'elasticsearch-head/i18n.js', + 'elasticsearch-head/onload.js', + 'elasticsearch-head/base/favicon.png', + 'elasticsearch-head/base/jquery.min.js', + 'elasticsearch-head/base/loading.gif', + 'elasticsearch-head/base/purecss.css', + 'elasticsearch-head/base/reset.css', + 'elasticsearch-head/fonts/fontawesome-webfont.eot', + 'elasticsearch-head/fonts/fontawesome-webfont.svg', + 'elasticsearch-head/fonts/fontawesome-webfont.ttf', + 'elasticsearch-head/fonts/fontawesome-webfont.woff', + 'elasticsearch-head/fonts/FontAwesome.otf', + 'elasticsearch-head/lang/en_strings.js', + 'elasticsearch-head/lang/fr_strings.js', + 'elasticsearch-head/lang/ja_strings.js', + 'elasticsearch-head/lang/pt_strings.js', + 'elasticsearch-head/lang/tr_strings.js', + 'elasticsearch-head/lang/zh_strings.js', + 'elasticsearch-head/lang/zh-TW_strings.js', + 'icons/esmhead_16x16.png', + 'icons/esmhead_32x32.png', + 'icons/esmhead_48x48.png', + 'icons/esmhead_128x128.png' +] + +for file in files_to_copy: + source_path = file + dest_path = os.path.join(build_dir, file) + + if os.path.exists(source_path): + dest_dir = os.path.dirname(dest_path) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir, exist_ok=True) + + shutil.copy2(source_path, dest_path) + print(f"✓ Copied: {file}") + else: + print(f"✗ Missing: {file}") + +print('\nBuild completed successfully!') +print('Extension files are ready in the "build" directory.') +print('You can now load the extension in Chrome from the build directory.') + +create_zip = input('\nDo you want to create a ZIP file for distribution? (y/n): ').lower().strip() +if create_zip == 'y': + zip_filename = 'elasticsearch-head-custom-v0.4.4.zip' + + with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: + for root, dirs, files in os.walk(build_dir): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, build_dir) + zipf.write(file_path, arcname) + print(f"✓ Added to ZIP: {arcname}") + + print(f'\nZIP file created: {zip_filename}') + print('This ZIP file can be uploaded to the Chrome Web Store or distributed manually.') diff --git a/doc/esmhead_custom_sc.png b/doc/esmhead_custom_sc.png new file mode 100644 index 0000000..9e3821c Binary files /dev/null and b/doc/esmhead_custom_sc.png differ diff --git a/doc/esmhead_custom_sc0.png b/doc/esmhead_custom_sc0.png new file mode 100644 index 0000000..0b54275 Binary files /dev/null and b/doc/esmhead_custom_sc0.png differ diff --git a/elasticsearch-head/app.css b/elasticsearch-head/app.css index 0e1c139..651f929 100644 --- a/elasticsearch-head/app.css +++ b/elasticsearch-head/app.css @@ -114,38 +114,66 @@ TABLE.table H3 { text-align: left; } -.uiSplitButton { - white-space: nowrap; +.uiRefreshButton { + display: inline-flex; + align-items: center; + gap: 8px; + background: #96c6eb; + border: 1px solid #668dc6; + border-radius: 4px; + padding: 4px; + color: white; } -.uiSplitButton .uiButton:first-child { - margin-right: 0; - display: inline-block; +.uiRefreshButton-select { + background: transparent; + border: none; + color: white; + font-size: 13px; + padding: 4px 8px; + cursor: pointer; + outline: none; } -.uiSplitButton .uiButton:first-child .uiButton-content { - border-right-width: 1; - border-right-color: #5296c7; - border-top-right-radius: 0; - border-bottom-right-radius: 0; +.uiRefreshButton-select option { + background: #96c6eb; + color: white; +} + +.uiRefreshButton-icon { + background: transparent; + border: none; + color: white; + cursor: pointer; + padding: 4px 6px; + border-radius: 3px; + transition: background-color 0.2s ease; + display: flex; + align-items: center; + justify-content: center; } -.uiSplitButton .uiMenuButton { - margin-left: 0; +.uiRefreshButton-icon:hover { + background: rgba(255, 255, 255, 0.2); } -.uiSplitButton .uiButton:last-child .uiButton-content { - border-radius: 2px; - border-left-width: 1; - border-left-color: #96c6eb; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - height: 15px; +.uiRefreshButton-icon.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.uiRefreshButton-icon.disabled:hover { + background: transparent; +} + +.refresh-icon { + font-size: 14px; + font-weight: bold; } -.uiSplitButton .uiButton:last-child .uiButton-label { - padding: 2px 17px 2px 6px; - margin-left: -8px; +.uiRefreshButton.auto-refresh { + background: #2575b7; + border-color: #1e5a8a; } .uiToolbar { @@ -258,11 +286,14 @@ TABLE.table H3 { position: absolute; background: #96c6eb; color: white; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .uiMenuPanel LI { list-style: none; border-bottom: 1px solid #668dc6; + padding: 0; } .uiMenuPanel LI:hover { @@ -273,7 +304,13 @@ TABLE.table H3 { border-bottom: 0; } +.uiMenuPanel-item { + cursor: pointer; +} + .uiMenuPanel-label { + font-size: 13px; + line-height: 1.4; white-space: nowrap; padding: 2px 10px 2px 10px; cursor: pointer; @@ -764,6 +801,12 @@ SPAN.uiJsonPretty-boolean { top: 1px; } +.uiFilterBrowser-row BUTTON { + height: 22px; + position: relative; + top: 1px; +} + .uiHeader { padding: 3px 10px; } @@ -827,6 +870,95 @@ SPAN.uiJsonPretty-boolean { margin-right: 5px; } +.uiIndexSelector { + display: inline; +} + +.uiIndexSelector-customSelect { + position: relative; + display: inline-block; +} + +.uiIndexSelector-selectInput { + padding: 4px 8px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 100%; + width: auto; + min-width: 200px; + background: #fff; + box-sizing: border-box; +} + +.uiIndexSelector-selectInput:focus { + outline: none; + border-color: #397fca; + box-shadow: 0 0 3px rgba(57, 127, 202, 0.3); +} + +.uiIndexSelector-selectInput::placeholder { + color: #999; + font-style: italic; +} + +.uiIndexSelector-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + display: none; +} + +.uiIndexSelector-option { + padding: 8px 10px; + cursor: pointer; + border-bottom: 1px solid #eee; +} + +.uiIndexSelector-option:hover { + background-color: #f5f5f5; +} + +.uiIndexSelector-option:last-child { + border-bottom: none; +} + +.uiIndexOverview-deleteButton { + margin-left: 10px; + background-color: #dc3545; + border-color: #dc3545; + color: white; +} + +.uiIndexOverview-deleteButton:hover { + background-color: #c82333; + border-color: #bd2130; +} + +.uiIndexOverview-deleteButton:disabled { + background-color: #6c757d; + border-color: #6c757d; + cursor: not-allowed; +} + +.uiIndexOverview-checkbox { + margin: 0; + cursor: pointer; +} + +.uiIndexOverview-selectedCount { + margin-left: 10px; + color: #6c757d; + font-size: 0.9em; +} + .modalDialog { display: none; position: fixed; diff --git a/elasticsearch-head/app.js b/elasticsearch-head/app.js index 4668094..2875ece 100644 --- a/elasticsearch-head/app.js +++ b/elasticsearch-head/app.js @@ -1843,13 +1843,17 @@ (function ($, app, i18n) { var ui = app.ns("ui"); - ui.RefreshButton = ui.SplitButton.extend({ + ui.RefreshButton = ui.AbstractWidget.extend({ defaults: { timer: -1, }, init: function (parent) { - this.config.label = i18n.text("General.RefreshResults"); this._super(parent); + this.value = this.config.timer; + this.el = $(this._main_template()); + this.selectEl = this.el.find(".uiRefreshButton-select"); + this.iconEl = this.el.find(".uiRefreshButton-icon"); + this.attach(parent); this.set(this.config.timer); }, set: function (value) { @@ -1857,25 +1861,57 @@ window.clearInterval(this._timer); if (this.value > 0) { this._timer = window.setInterval(this._refresh_handler, this.value); + this.el.addClass("auto-refresh"); + } else { + this.el.removeClass("auto-refresh"); } }, - _click_handler: function () { - this._refresh_handler(); + disable: function () { + this.selectEl.prop("disabled", true); + this.iconEl.addClass("disabled"); }, - _select_handler: function (el, event) { - this.set(event.value); - this.fire("change", this); + enable: function () { + this.selectEl.prop("disabled", false); + this.iconEl.removeClass("disabled"); }, _refresh_handler: function () { this.fire("refresh", this); }, - _getItems: function () { - return [ - { text: i18n.text("General.ManualRefresh"), value: -1 }, - { text: i18n.text("General.RefreshQuickly"), value: 100 }, - { text: i18n.text("General.Refresh5seconds"), value: 5000 }, - { text: i18n.text("General.Refresh1minute"), value: 60000 }, - ]; + _select_handler: function (jEv) { + var value = parseInt($(jEv.target).val()); + this.set(value); + this.fire("change", this); + }, + _icon_click_handler: function () { + this._refresh_handler(); + }, + _main_template: function () { + return { + tag: "DIV", + cls: "uiRefreshButton", + children: [ + { + tag: "SELECT", + cls: "uiRefreshButton-select", + onchange: this._select_handler, + children: [ + { tag: "OPTION", value: "-1", text: i18n.text("General.ManualRefresh") }, + { tag: "OPTION", value: "5000", text: i18n.text("General.Refresh5seconds") }, + { tag: "OPTION", value: "15000", text: i18n.text("General.Refresh15seconds") }, + { tag: "OPTION", value: "30000", text: i18n.text("General.Refresh30seconds") }, + ], + }, + { + tag: "BUTTON", + cls: "uiRefreshButton-icon", + type: "button", + onclick: this._icon_click_handler, + children: [ + { tag: "SPAN", cls: "refresh-icon", text: "↻" } + ], + }, + ], + }; }, }); })(this.jQuery, this.app, this.i18n); @@ -2133,11 +2169,31 @@ _getPosition: function (jEv) { var right = !!$(jEv.target).parents(".pull-right").length; var parent = $(jEv.target).closest("BUTTON"); - return parent + var basePosition = parent .vOffset() - .addY(parent.vSize().y) - .addX(right ? parent.vSize().x - this.el.vOuterSize().x : 0) - .asOffset(); + .addY(parent.vSize().y); + + if (right) { + basePosition = basePosition.addX(parent.vSize().x - this.el.vOuterSize().x); + } + + var windowWidth = $(window).width(); + var windowHeight = $(window).height(); + var menuWidth = this.el.outerWidth(); + var menuHeight = this.el.outerHeight(); + + if (basePosition.x + menuWidth > windowWidth) { + basePosition = basePosition.addX(windowWidth - basePosition.x - menuWidth - 10); + } + + if (basePosition.y + menuHeight > windowHeight) { + basePosition = basePosition.addY(-parent.vSize().y - menuHeight); + } + + if (basePosition.x < 10) basePosition = basePosition.addX(10 - basePosition.x); + if (basePosition.y < 10) basePosition = basePosition.addY(10 - basePosition.y); + + return basePosition.asOffset(); }, }); })(this.app); @@ -4433,7 +4489,10 @@ this.prefs = services.Preferences.instance(); this.cluster = this.config.cluster; this.el = $.joey(this._main_template()); - this._updateServerSelect(); + + var currentUri = this.cluster ? this.cluster.base_uri : null; + this._updateServerSelect(currentUri); + this.cluster.get("", this._node_handler); $("body").bind("onconnect", this._addServer_handler); }, @@ -4447,6 +4506,11 @@ _reconnect_handler: function () { var base_uri = this.el.find(".uiClusterConnect-uri").val(); + + if (!base_uri) { + return; + } + var url; if (base_uri.indexOf("?") !== -1) { url = base_uri.substring(0, base_uri.indexOf("?") - 1); @@ -4484,10 +4548,10 @@ var options = []; var added = false; if (cluster_list) { - for (i in cluster_list) { - name = cluster_list[i].name; - uri = cluster_list[i].uri; - version = cluster_list[i].version; + for (var i in cluster_list) { + var name = cluster_list[i].name; + var uri = cluster_list[i].uri; + var version = cluster_list[i].version; if (!uri) continue; if (uri.indexOf("?") != -1) continue; options.push(this._optionCluster_template(uri, name, version)); @@ -4497,41 +4561,74 @@ } } if (select_uri && !added) { - options.push(this._optionCluster_template(uri, select_name)); + options.push(this._optionCluster_template(select_uri, select_name)); } this.el.find(".uiClusterSelector-select").empty().append(this._selectCluster_template(options)); if (select_uri) { this.el.find(".uiClusterSelector-select select").val(select_uri); + } else if (options.length > 0) { + var firstOption = options[0]; + this.el.find(".uiClusterSelector-select select").val(firstOption.value); + } else { + this.el.find(".uiClusterSelector-select select").val(""); } }, _addServer_handler: function (event, data) { var base_uri = data.base_uri; - //this.el.find(".uiClusterSelector-select select").val();//this.el.find(".uiClusterConnect-uri").val(); - cluster_list = this.prefs.get("app-cluster_list"); + var cluster_list = this.prefs.get("app-cluster_list"); if (!cluster_list) { cluster_list = {}; } + + if (cluster_list[base_uri]) { + return; + } + cluster_list[data.base_uri] = { uri: data.base_uri, name: data.cluster_name, version: data.cluster_version, }; - for (x in cluster_list) { - if (x[x.length - 1] != "/") delete cluster_list[x]; // Clean up URL without trailing space. - } + this.prefs.set("app-cluster_list", cluster_list); this._updateServerSelect(base_uri, data.cluster_name); }, _removeServer_handler: function () { - cluster_list = this.prefs.get("app-cluster_list"); - delete cluster_list[this.el.find(".uiClusterSelector-select select").val()]; - this.prefs.set("app-cluster_list", cluster_list); + var selectedUri = this.el.find(".uiClusterSelector-select select").val(); + + if (!selectedUri) { + return; + } + + var cluster_list = this.prefs.get("app-cluster_list"); + + if (cluster_list && cluster_list[selectedUri]) { + delete cluster_list[selectedUri]; + this.prefs.set("app-cluster_list", cluster_list); + } else { + return; + } + this._updateServerSelect(); - this._reconnect_handler(); + + var remainingServers = Object.keys(cluster_list || {}); + if (remainingServers.length > 0) { + var firstServer = remainingServers[0]; + this.el.find(".uiClusterSelector-select select").val(firstServer); + this.el.find(".uiClusterConnect-uri").val(firstServer); + this._reconnect_handler(); + } else { + this.el.find(".uiClusterConnect-uri").val(""); + } }, _changeCluster_handler: function () { var uri = this.el.find(".uiClusterSelector-select select").val(); + + if (!uri) { + return; + } + this.el.find(".uiClusterConnect-uri").val(uri); this._reconnect_handler(); this._updateServerSelect(uri, i18n.text("AnyRequest.Requesting")); @@ -4568,9 +4665,25 @@ urlObject.password = data.password || ""; } - this.el.find(".uiClusterConnect-uri").val(urlObject.toString()); + var uri = urlObject.toString(); + + var cluster_list = this.prefs.get("app-cluster_list"); + if (!cluster_list) { + cluster_list = {}; + } + + cluster_list[uri] = { + uri: uri, + name: "New Server", + version: null + }; + + this.prefs.set("app-cluster_list", cluster_list); + + this._updateServerSelect(uri, "New Server"); + + this.el.find(".uiClusterConnect-uri").val(uri); this._reconnect_handler(); - this._updateServerSelect(uri, i18n.text("AnyRequest.Requesting")); } }.bind(this), }).open(); @@ -5018,6 +5131,7 @@ this.el = $(this._main_template()); this.attach(parent); this.cluster = this.config.cluster; + this.allIndices = []; this.update(); }, update: function () { @@ -5025,13 +5139,116 @@ }, _update_handler: function (data) { - var options = []; + this.allIndices = []; var index_names = Object.keys(data.indices).sort(); for (var i = 0; i < index_names.length; i++) { - name = index_names[i]; - options.push(this._option_template(name, data.indices[name])); + var name = index_names[i]; + this.allIndices.push({ + name: name, + data: data.indices[name] + }); } - this.el.find(".uiIndexSelector-select").empty().append(this._select_template(options)); + setTimeout(function() { + this._initializeOptions(); + }.bind(this), 0); + }, + + _updateOptions: function() { + var filterInput = this.el.find(".uiIndexSelector-selectInput"); + var filterValue = filterInput.length > 0 ? filterInput.val() || "" : ""; + filterValue = filterValue.toLowerCase(); + + var filteredIndices = this.allIndices.filter(function(index) { + return index.name.toLowerCase().includes(filterValue); + }); + + var dropdown = this.el.find(".uiIndexSelector-dropdown"); + if (dropdown.length > 0) { + dropdown.empty(); + + var maxLength = 0; + var optionTexts = []; + + for (var i = 0; i < filteredIndices.length; i++) { + var index = filteredIndices[i]; + var optionText = i18n.text("IndexSelector.NameWithDocs", index.name, index.data.primaries.docs.count); + optionTexts.push(optionText); + maxLength = Math.max(maxLength, optionText.length); + } + + if (maxLength > 0) { + var estimatedWidth = Math.max(200, maxLength * 8); + filterInput.css('width', estimatedWidth + 'px'); + } + + for (var j = 0; j < filteredIndices.length; j++) { + var index = filteredIndices[j]; + var optionEl = $("
") + .addClass("uiIndexSelector-option") + .attr("data-value", index.name) + .text(optionTexts[j]) + .click(function(name) { + return function() { + this._optionSelected_handler(name); + }.bind(this); + }.bind(this)(index.name)); + dropdown.append(optionEl); + } + } + }, + + _filterChanged_handler: function() { + this._updateOptions(); + }, + + _showDropdown_handler: function() { + this.el.find(".uiIndexSelector-dropdown").show(); + }, + + _hideDropdown_handler: function() { + setTimeout(function() { + this.el.find(".uiIndexSelector-dropdown").hide(); + }.bind(this), 200); + }, + + _initializeOptions: function() { + var options = []; + var maxLength = 0; + + for (var i = 0; i < this.allIndices.length; i++) { + var index = this.allIndices[i]; + var optionText = i18n.text("IndexSelector.NameWithDocs", index.name, index.data.primaries.docs.count); + maxLength = Math.max(maxLength, optionText.length); + options.push(this._option_template(index.name, index.data)); + } + + var selectContainer = this.el.find(".uiIndexSelector-select"); + if (selectContainer.length > 0) { + selectContainer.empty().append(this._select_template(options)); + + if (maxLength > 0) { + var estimatedWidth = Math.max(200, maxLength * 8); + this.el.find(".uiIndexSelector-selectInput").css('width', estimatedWidth + 'px'); + } + + var self = this; + this.el.find(".uiIndexSelector-selectInput").on("keyup", function() { + self._filterChanged_handler(); + }); + + this.el.find(".uiIndexSelector-selectInput").on("focus", function() { + self._showDropdown_handler(); + }); + + this.el.find(".uiIndexSelector-selectInput").on("blur", function() { + self._hideDropdown_handler(); + }); + } + }, + + _optionSelected_handler: function(name) { + this.el.find(".uiIndexSelector-selectInput").val(name); + this.el.find(".uiIndexSelector-dropdown").hide(); this._indexChanged_handler(); }, @@ -5042,23 +5259,41 @@ children: i18n.complex("IndexSelector.SearchIndexForDocs", { tag: "SPAN", cls: "uiIndexSelector-select", - }), + }) }; }, _indexChanged_handler: function () { - this.fire("indexChanged", this.el.find("SELECT").val()); + this.fire("indexChanged", this.el.find(".uiIndexSelector-selectInput").val()); }, _select_template: function (options) { - return { tag: "SELECT", children: options, onChange: this._indexChanged_handler }; + return { + tag: "DIV", + cls: "uiIndexSelector-customSelect", + children: [ + { + tag: "INPUT", + type: "text", + cls: "uiIndexSelector-selectInput", + placeholder: i18n.text("IndexSelector.FilterPlaceholder", "Type to filter indexes...") + }, + { + tag: "DIV", + cls: "uiIndexSelector-dropdown", + children: options + } + ] + }; }, _option_template: function (name, index) { return { - tag: "OPTION", - value: name, + tag: "DIV", + cls: "uiIndexSelector-option", + "data-value": name, text: i18n.text("IndexSelector.NameWithDocs", name, index.primaries.docs.count), + onclick: this._optionSelected_handler.bind(this, name) }; }, }); @@ -5155,6 +5390,16 @@ this.cluster = this.config.cluster; this._clusterState = this.config.clusterState; this._clusterState.on("data", this._refresh_handler); + + this.selectedIndices = []; + this.maxSelection = 5; + + this._deleteButton = new ui.Button({ + label: i18n.text("IndexOverview.DeleteSelected"), + cls: "uiIndexOverview-deleteButton", + onclick: this._deleteSelected_handler, + }); + this.el = $(this._main_template()); this._refresh_handler(); }, @@ -5249,7 +5494,17 @@ { tag: "TR", children: [ - { tag: "TH" }, + { + tag: "TH", + children: [ + { + tag: "INPUT", + type: "checkbox", + cls: "uiIndexOverview-checkbox", + onclick: this._selectAll_handler + } + ] + }, { tag: "TH", children: [{ tag: "STRONG", text: "Aliases" }] }, { tag: "TH", children: [{ tag: "STRONG", text: "Creation Date" }] }, @@ -5272,6 +5527,18 @@ return { tag: "TR", children: [ + { + tag: "TD", + children: [ + { + tag: "INPUT", + type: "checkbox", + cls: "uiIndexOverview-checkbox", + "data-index": index.name, + onclick: this._selectIndex_handler.bind(this, index.name) + } + ] + }, { tag: "TD", children: [{ tag: "STRONG", text: index.name }] }, { tag: "TD", text: index.aliases ? index.aliases.join(",") : "" }, { tag: "TD", text: index.creationDate }, @@ -5301,12 +5568,126 @@ label: i18n.text("ClusterOverview.NewIndex"), onclick: this._newIndex_handler, }), + this._deleteButton, + { + tag: "SPAN", + cls: "uiIndexOverview-selectedCount", + text: i18n.text("IndexOverview.SelectMax") + } ], }), { tag: "DIV", cls: "uiIndexOverview-table", children: this._indexViewEl }, ], }; }, + + _selectIndex_handler: function(indexName, ev) { + var checkbox = ev.target; + var isChecked = checkbox.checked; + + if (isChecked) { + if (this.selectedIndices.length >= this.maxSelection) { + checkbox.checked = false; + alert(i18n.text("IndexOverview.TooManySelected")); + return; + } + this.selectedIndices.push(indexName); + } else { + var index = this.selectedIndices.indexOf(indexName); + if (index > -1) { + this.selectedIndices.splice(index, 1); + } + } + + this._updateDeleteButton(); + }, + + _selectAll_handler: function(ev) { + var checkbox = ev.target; + var isChecked = checkbox.checked; + var indexCheckboxes = this.el.find("tbody input[type='checkbox']"); + + if (isChecked) { + var maxToSelect = Math.min(this.maxSelection, indexCheckboxes.length); + indexCheckboxes.each(function(i, cb) { + if (i < maxToSelect) { + cb.checked = true; + var indexName = $(cb).data("index"); + if (this.selectedIndices.indexOf(indexName) === -1) { + this.selectedIndices.push(indexName); + } + } + }.bind(this)); + } else { + indexCheckboxes.prop("checked", false); + this.selectedIndices = []; + } + + this._updateDeleteButton(); + }, + + _updateDeleteButton: function() { + this._deleteButton.el.prop("disabled", this.selectedIndices.length === 0); + }, + + _deleteSelected_handler: function() { + if (this.selectedIndices.length === 0) { + alert(i18n.text("IndexOverview.NoIndexSelected")); + return; + } + + var selectedNames = this.selectedIndices.join(", "); + + if (prompt(i18n.text("IndexOverview.DeleteMessage", i18n.text("Command.DELETE"), selectedNames)) === i18n.text("Command.DELETE")) { + this._performDelete(); + } + }, + + _performDelete: function() { + var self = this; + var deletedCount = 0; + var failedCount = 0; + var totalCount = this.selectedIndices.length; + + var deleteNext = function(index) { + if (index >= self.selectedIndices.length) { + if (failedCount === 0) { + alert(i18n.text("IndexOverview.DeleteSuccess")); + } else { + alert(i18n.text("IndexOverview.DeleteFailed")); + } + self.selectedIndices = []; + self._updateDeleteButton(); + + // self._clusterState.refresh(); + return; + } + + var indexName = self.selectedIndices[index]; + + setTimeout(function() { + self.cluster.delete( + encodeURIComponent(indexName), + null, + function(response) { + deletedCount++; + setTimeout(function() { + deleteNext(index + 1); + }, 2000); + }, + function(error) { + console.error("Failed to delete index:", indexName, error); + failedCount++; + setTimeout(function() { + deleteNext(index + 1); + }, 2000); + } + ); + }, 1000); + }; + + deleteNext(0); + }, }); })(this.jQuery, this.app, this.i18n); diff --git a/elasticsearch-head/lang/en_strings.js b/elasticsearch-head/lang/en_strings.js index 8e164e6..2615145 100644 --- a/elasticsearch-head/lang/en_strings.js +++ b/elasticsearch-head/lang/en_strings.js @@ -8,9 +8,9 @@ i18n.setKeys({ "General.CloseGlyph": "X", "General.RefreshResults": "Refresh", "General.ManualRefresh": "Manual Refresh", - "General.RefreshQuickly": "Refresh quickly", "General.Refresh5seconds": "Refresh every 5 seconds", - "General.Refresh1minute": "Refresh every minute", + "General.Refresh15seconds": "Refresh every 15 seconds", + "General.Refresh30seconds": "Refresh every 30 seconds", "AliasForm.AliasName": "Alias Name", "AliasForm.NewAliasForIndex": "New Alias for {0}", "AliasForm.DeleteAliasMessage": "type ''{0}'' to delete {1}. There is no undo", @@ -56,6 +56,14 @@ i18n.setKeys({ "IndexOverview.PageTitle": "Indices Overview", "IndexSelector.NameWithDocs": "{0} ({1} docs)", "IndexSelector.SearchIndexForDocs": "Search {0} for documents where:", + "IndexSelector.FilterPlaceholder": "Type to filter indexes...", + "IndexOverview.DeleteSelected": "Delete Selected", + "IndexOverview.SelectMax": "Select up to 5 indexes", + "IndexOverview.DeleteMessage": "type ''{0}'' to delete {1}. There is no undo", + "IndexOverview.NoIndexSelected": "Please select at least one index to delete", + "IndexOverview.TooManySelected": "You can select up to 5 indexes at once", + "IndexOverview.DeleteSuccess": "Selected indexes have been deleted successfully. Please refresh to see the updated list.", + "IndexOverview.DeleteFailed": "Failed to delete some indexes", "FilterBrowser.OutputType": "Output Results: {0}", "FilterBrowser.OutputSize": "Number of Results: {0}", "Header.ClusterHealth": "cluster health: {0} ({1} of {2})", diff --git a/elasticsearch-head/lang/fr_strings.js b/elasticsearch-head/lang/fr_strings.js index b17d712..062218f 100644 --- a/elasticsearch-head/lang/fr_strings.js +++ b/elasticsearch-head/lang/fr_strings.js @@ -8,9 +8,9 @@ i18n.setKeys({ // "General.CloseGlyph": "X", "General.RefreshResults": "Rafraîchir", "General.ManualRefresh": "Rafraîchissement manuel", - "General.RefreshQuickly": "Rafraîchissement rapide", "General.Refresh5seconds": "Rafraîchissement toutes les 5 secondes", - "General.Refresh1minute": "Rafraîchissement toutes les minutes", + "General.Refresh15seconds": "Rafraîchissement toutes les 15 secondes", + "General.Refresh30seconds": "Rafraîchissement toutes les 30 secondes", "AliasForm.AliasName": "Alias", "AliasForm.NewAliasForIndex": "Nouvel Alias pour {0}", "AliasForm.DeleteAliasMessage": "Entrez ''{0}'' pour effacer {1}. Attention, action irréversible.", @@ -55,6 +55,14 @@ i18n.setKeys({ "IndexCommand.ShutdownMessage": "Entrez ''{0}'' pour éteindre {1}. Le noeud NE PEUT PAS être redémarré depuis cette interface.", // "IndexSelector.NameWithDocs": "{0} ({1} docs)", "IndexSelector.SearchIndexForDocs": "Chercher dans {0} les documents correspondant à", + "IndexSelector.FilterPlaceholder": "Tapez pour filtrer les index...", + "IndexOverview.DeleteSelected": "Supprimer la Sélection", + "IndexOverview.SelectMax": "Sélectionnez jusqu'à 5 index", + "IndexOverview.DeleteMessage": "Entrez ''{0}'' pour effacer {1}. Attention, action irréversible.", + "IndexOverview.NoIndexSelected": "Veuillez sélectionner au moins un index à supprimer", + "IndexOverview.TooManySelected": "Vous pouvez sélectionner jusqu'à 5 index à la fois", + "IndexOverview.DeleteSuccess": "Les index sélectionnés ont été supprimés avec succès. Veuillez actualiser pour voir la liste mise à jour.", + "IndexOverview.DeleteFailed": "Échec de la suppression de certains index", "FilterBrowser.OutputType": "Format d'affichage des résultats {0}", "FilterBrowser.OutputSize": "Nombre de Résultats: {0}", "Header.ClusterHealth": "Santé du cluster: {0} ({1} {2})", diff --git a/elasticsearch-head/lang/ja_strings.js b/elasticsearch-head/lang/ja_strings.js index 171b0c6..150c4f4 100644 --- a/elasticsearch-head/lang/ja_strings.js +++ b/elasticsearch-head/lang/ja_strings.js @@ -8,9 +8,9 @@ i18n.setKeys({ // "General.CloseGlyph": "X", "General.RefreshResults": "リフレッシュ", "General.ManualRefresh": "マニュアルモード", - "General.RefreshQuickly": "クイックモード", "General.Refresh5seconds": "5秒毎にリフレッシュ", - "General.Refresh1minute": "1分毎にリフレッシュ", + "General.Refresh15seconds": "15秒毎にリフレッシュ", + "General.Refresh30seconds": "30秒毎にリフレッシュ", "AliasForm.AliasName": "エイリアス名", "AliasForm.NewAliasForIndex": "{0} の新しいエイリアス", "AliasForm.DeleteAliasMessage": "インデックス ''{1}'' を削除するために ''{0}'' とタイプして下さい. この操作は undo できません.", @@ -56,6 +56,14 @@ i18n.setKeys({ "IndexOverview.PageTitle": "インデックスのOverview", // "IndexSelector.NameWithDocs": "{0} ({1} docs)", "IndexSelector.SearchIndexForDocs": "Search {0} for documents where:", + "IndexSelector.FilterPlaceholder": "インデックスをフィルタするには入力してください...", + "IndexOverview.DeleteSelected": "選択削除", + "IndexOverview.SelectMax": "最大5つのインデックスを選択してください", + "IndexOverview.DeleteMessage": "インデックス ''{1}'' を削除するために ''{0}'' とタイプして下さい. この操作は undo できません.", + "IndexOverview.NoIndexSelected": "削除するインデックスを少なくとも1つ選択してください", + "IndexOverview.TooManySelected": "一度に最大5つのインデックスを選択できます", + "IndexOverview.DeleteSuccess": "選択されたインデックスが正常に削除されました。更新されたリストを表示するには更新してください。", + "IndexOverview.DeleteFailed": "一部のインデックスの削除に失敗しました", "FilterBrowser.OutputType": "結果の出力形式: {0} ", "FilterBrowser.OutputSize": "結果の取得サイズ: {0} ", "Header.ClusterHealth": "cluster health: {0} ({1} of {2})", diff --git a/elasticsearch-head/lang/pt_strings.js b/elasticsearch-head/lang/pt_strings.js index d1066ba..0df0724 100644 --- a/elasticsearch-head/lang/pt_strings.js +++ b/elasticsearch-head/lang/pt_strings.js @@ -8,9 +8,9 @@ i18n.setKeys({ "General.CloseGlyph": "X", "General.RefreshResults": "Atualizar", "General.ManualRefresh": "Atualização Manual", - "General.RefreshQuickly": "Atualização rápida", "General.Refresh5seconds": "Atualização a cada 5 segundos", - "General.Refresh1minute": "Atualização a cada minuto", + "General.Refresh15seconds": "Atualização a cada 15 segundos", + "General.Refresh30seconds": "Atualização a cada 30 segundos", "AliasForm.AliasName": "Apelido", "AliasForm.NewAliasForIndex": "Novo apelido para {0}", "AliasForm.DeleteAliasMessage": "digite ''{0}'' para deletar {1}. Não há como voltar atrás", @@ -32,7 +32,7 @@ i18n.setKeys({ "Browser.ResultSourcePanelTitle": "Resultado", "Command.DELETE": "DELETAR", "Command.SHUTDOWN": "DESLIGAR", - "Command.DeleteAliasMessage": "Remover apelido?", + "Command.DeleteAliasMessage": "Remover apelido ''{0}'' do índice ''{1}''?", "ClusterOverView.IndexName": "Nome do índice", "ClusterOverview.NumShards": "Número de Shards", "ClusterOverview.NumReplicas": "Número de Réplicas", @@ -54,13 +54,24 @@ i18n.setKeys({ "IndexCommand.TextToAnalyze": "Texto para analizar", "IndexCommand.ShutdownMessage": "digite ''{0}'' para desligar {1}. Nó NÃO PODE ser reiniciado à partir dessa interface", "IndexOverview.PageTitle": "Visão geral dos índices", - "IndexSelector.NameWithDocs": "{0} ({1} documentoss)", + "IndexSelector.NameWithDocs": "{0} ({1} documentos)", "IndexSelector.SearchIndexForDocs": "Busca {0} por documentos onde:", + "IndexSelector.FilterPlaceholder": "Digite para filtrar índices...", + "IndexOverview.DeleteSelected": "Deletar Selecionados", + "IndexOverview.SelectMax": "Selecione até 5 índices", + "IndexOverview.DeleteMessage": "digite ''{0}'' para deletar {1}. Não há como voltar atrás", + "IndexOverview.NoIndexSelected": "Por favor, selecione pelo menos um índice para deletar", + "IndexOverview.TooManySelected": "Você pode selecionar até 5 índices de uma vez", + "IndexOverview.DeleteSuccess": "Índices selecionados foram deletados com sucesso. Por favor, atualize para ver a lista atualizada.", + "IndexOverview.DeleteFailed": "Falha ao deletar alguns índices", "FilterBrowser.OutputType": "Resultados: {0}", "FilterBrowser.OutputSize": "Número de Resultados: {0}", "Header.ClusterHealth": "saúde do cluster: {0} ({1} {2})", "Header.ClusterNotConnected": "saúde do cluster: não conectado", "Header.Connect": "Conectar", + "Header.NewConnect" : "Novo", + "Header.ServerListAdd": "+", + "Header.ServerListRemove": "-", "Nav.AnyRequest": "Qualquer requisição", "Nav.Browser": "Navegador", "Nav.ClusterHealth": "Saúde do Cluster", @@ -103,10 +114,17 @@ i18n.setKeys({ "Sort.ByName": "Por nome", "Sort.ByAddress": "Por endereço", "Sort.ByType": "Por tipo", + "Preference.SortIndices": "Ordenar Índices", + "SortIndices.Descending": "Decrescente", + "SortIndices.Ascending": "Crescente", "Preference.ViewAliases": "Ver Alias", "ViewAliases.Grouped": "Agrupado", "ViewAliases.List": "Lista", "ViewAliases.None": "Nenhum", + "Preference.ViewIndexSelect": "Ver Índices", + "ViewIndexSelect.System": "Sistema", + "ViewIndexSelect.Normal": "Normal", + "ViewIndexSelect.Both": "Ambos", "Overview.IndexFilter": "Filtar Índice", "TableResults.Summary": "Buscado {0} de {1} shards. {2} resultados. {3} segundos", "QueryFilter.AllIndices": "Todos os Índices", diff --git a/elasticsearch-head/lang/tr_strings.js b/elasticsearch-head/lang/tr_strings.js index 6b4cda3..776c304 100644 --- a/elasticsearch-head/lang/tr_strings.js +++ b/elasticsearch-head/lang/tr_strings.js @@ -8,8 +8,9 @@ i18n.setKeys({ "General.CloseGlyph": "X", "General.RefreshResults": "Yenile", "General.ManualRefresh": "Manuel Yenileme", - "General.RefreshQuickly": "Hızlı yenile", "General.Refresh5seconds": "5 saniyede bir yenile", + "General.Refresh15seconds": "15 saniyede bir yenile", + "General.Refresh30seconds": "30 saniyede bir yenile", "General.Refresh1minute": "Her dakika yenile", "AliasForm.AliasName": "Alternatif İsim", "AliasForm.NewAliasForIndex": "{0} için yeni alternatif isim", @@ -56,6 +57,14 @@ i18n.setKeys({ "IndexOverview.PageTitle": "Indeksler Genel Bakış", "IndexSelector.NameWithDocs": "{0} ({1} döküman)", "IndexSelector.SearchIndexForDocs": "{0} indeksinde ara:", + "IndexSelector.FilterPlaceholder": "İndeksleri filtrelemek için yazın...", + "IndexOverview.DeleteSelected": "Seçilenleri Sil", + "IndexOverview.SelectMax": "En fazla 5 indeks seçin", + "IndexOverview.DeleteMessage": "{1} silmek için ''{0}'' . Geriye dönüş yoktur.", + "IndexOverview.NoIndexSelected": "Silmek için en az bir indeks seçin", + "IndexOverview.TooManySelected": "Bir seferde en fazla 5 indeks seçebilirsiniz", + "IndexOverview.DeleteSuccess": "Seçilen indeksler başarıyla silindi. Güncellenmiş listeyi görmek için yenileyin.", + "IndexOverview.DeleteFailed": "Bazı indeksler silinemedi", "FilterBrowser.OutputType": "Sonuç Formatı: {0}", "FilterBrowser.OutputSize": "Sonuç Sayısı: {0}", "Header.ClusterHealth": "Küme Durumu: {0} ({1} de {2})", diff --git a/elasticsearch-head/lang/zh-TW_strings.js b/elasticsearch-head/lang/zh-TW_strings.js index 5797deb..f34dad1 100644 --- a/elasticsearch-head/lang/zh-TW_strings.js +++ b/elasticsearch-head/lang/zh-TW_strings.js @@ -8,9 +8,9 @@ i18n.setKeys({ "General.CloseGlyph": "X", "General.RefreshResults": "更新", "General.ManualRefresh": "手動更新", - "General.RefreshQuickly": "快速更新", "General.Refresh5seconds": "每5秒更新", - "General.Refresh1minute": "每1分鐘更新", + "General.Refresh15seconds": "每15秒更新", + "General.Refresh30seconds": "每30秒更新", "AliasForm.AliasName": "别名", "AliasForm.NewAliasForIndex": "為 {0} 建立新别名", "AliasForm.DeleteAliasMessage": "輸入 ''{0}'' 删除 {1}. 此操作無法恢復", diff --git a/elasticsearch-head/lang/zh_strings.js b/elasticsearch-head/lang/zh_strings.js index cc5c272..22694b3 100644 --- a/elasticsearch-head/lang/zh_strings.js +++ b/elasticsearch-head/lang/zh_strings.js @@ -8,9 +8,9 @@ i18n.setKeys({ "General.CloseGlyph": "X", "General.RefreshResults": "刷新", "General.ManualRefresh": "手动刷新", - "General.RefreshQuickly": "快速刷新", "General.Refresh5seconds": "每5秒刷新", - "General.Refresh1minute": "每1分钟刷新", + "General.Refresh15seconds": "每15秒刷新", + "General.Refresh30seconds": "每30秒刷新", "AliasForm.AliasName": "别名", "AliasForm.NewAliasForIndex": "为 {0} 创建新别名", "AliasForm.DeleteAliasMessage": "输入 ''{0}'' 删除 {1}. 此操作无法恢复", @@ -56,6 +56,14 @@ i18n.setKeys({ "IndexOverview.PageTitle": "索引概览", "IndexSelector.NameWithDocs": "{0} ({1} 个文档)", "IndexSelector.SearchIndexForDocs": "搜索 {0} 的文档, 查询条件:", + "IndexSelector.FilterPlaceholder": "输入以过滤索引...", + "IndexOverview.DeleteSelected": "删除选中", + "IndexOverview.SelectMax": "最多选择5个索引", + "IndexOverview.DeleteMessage": "输入 ''{0}'' 删除 {1}. 此操作无法恢复", + "IndexOverview.NoIndexSelected": "请至少选择一个要删除的索引", + "IndexOverview.TooManySelected": "一次最多可以选择5个索引", + "IndexOverview.DeleteSuccess": "选中的索引已成功删除。请刷新以查看更新后的列表。", + "IndexOverview.DeleteFailed": "删除某些索引失败", "FilterBrowser.OutputType": "返回格式: {0}", "FilterBrowser.OutputSize": "显示数量: {0}", "Header.ClusterHealth": "集群健康值: {0} ({1} of {2})", diff --git a/icons/esmhead_128x128.png b/icons/esmhead_128x128.png index 705d69c..f178adb 100644 Binary files a/icons/esmhead_128x128.png and b/icons/esmhead_128x128.png differ diff --git a/icons/esmhead_16x16.png b/icons/esmhead_16x16.png index b332db2..7b843d5 100644 Binary files a/icons/esmhead_16x16.png and b/icons/esmhead_16x16.png differ diff --git a/icons/esmhead_32x32.png b/icons/esmhead_32x32.png index 9570f44..af46a32 100644 Binary files a/icons/esmhead_32x32.png and b/icons/esmhead_32x32.png differ diff --git a/icons/esmhead_48x48.png b/icons/esmhead_48x48.png index 6d6021f..0992181 100644 Binary files a/icons/esmhead_48x48.png and b/icons/esmhead_48x48.png differ diff --git a/manifest.json b/manifest.json index 719734a..d72ea36 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,9 @@ { "manifest_version": 3, - "name": "Multi Elasticsearch Heads", - "version": "0.4.3", - "description": "Manage and visualize multuple Elasticsearch endpoints from within this Chrome extension", + "name": "Multi Elasticsearch Head(Custom)", + "version": "0.4.4", + "version_name": "0.4.4-custom", + "description": "Custom version of Multi Elasticsearch Head with real-time index filtering for multi-tenant environments", "icons": { "16": "icons/esmhead_16x16.png", "32": "icons/esmhead_32x32.png",