diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b375e087..ec05643b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev + sudo apt-get install -y libwebkit2gtk-4.1-0=2.44.0-2 libwebkit2gtk-4.1-dev=2.44.0-2 libjavascriptcoregtk-4.1-0=2.44.0-2 libjavascriptcoregtk-4.1-dev=2.44.0-2 gir1.2-javascriptcoregtk-4.1=2.44.0-2 gir1.2-webkit2-4.1=2.44.0-2 build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev ### build apple arm diff --git a/CHANGELOG.md b/CHANGELOG.md index 162500d5..b39414ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +# 1.2.0 + +* adds support for date math in index names. You can use something like `` as your index name, when +searching and also in the `REST` view. fixes [#267](https://github.com/cars10/elasticvue/issues/267) + ## 1.1.2 This version re-adds the automatic updater for the desktop app. Since the update to tauri 2.0 diff --git a/browser_extension/chrome/manifest.json b/browser_extension/chrome/manifest.json index a75424b4..69871a00 100644 --- a/browser_extension/chrome/manifest.json +++ b/browser_extension/chrome/manifest.json @@ -1,6 +1,6 @@ { "name": "Elasticvue", - "version": "1.1.2", + "version": "1.2.0", "description": "Elasticsearch frontend", "manifest_version": 3, "icons": { diff --git a/browser_extension/firefox/manifest.json b/browser_extension/firefox/manifest.json index 1d2cdb1c..d85ed41c 100644 --- a/browser_extension/firefox/manifest.json +++ b/browser_extension/firefox/manifest.json @@ -1,6 +1,6 @@ { "name": "Elasticvue", - "version": "1.1.2", + "version": "1.2.0", "description": "Elasticsearch frontend", "manifest_version": 2, "icons": { diff --git a/package.json b/package.json index 431a7748..a6dba8b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "elasticvue", "private": true, - "version": "1.1.2", + "version": "1.2.0", "scripts": { "dev": "vite", "build": "vite build", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index efc4153d..bcbe5bbc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,8 @@ pub fn run() { env_logger::init(); let ctx = tauri::generate_context!(); + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); tauri::Builder::default() .plugin(tauri_plugin_process::init()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 76408f30..6e3b1446 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -42,7 +42,7 @@ }, "productName": "elasticvue", "mainBinaryName": "elasticvue", - "version": "1.1.2", + "version": "1.2.0", "identifier": "com.elasticvue.app", "plugins": { "updater": { diff --git a/src/assets/stylesheets/style.scss b/src/assets/stylesheets/style.scss index a23dc9fe..b12ee533 100644 --- a/src/assets/stylesheets/style.scss +++ b/src/assets/stylesheets/style.scss @@ -16,7 +16,6 @@ @import 'theme/components/list'; @import 'theme/components/reload_button'; @import 'theme/components/resizable'; -@import 'theme/components/shards'; @import 'theme/components/snackbar'; @import 'theme/components/table'; @import 'theme/components/text'; diff --git a/src/assets/stylesheets/theme/components/shards.css b/src/assets/stylesheets/theme/components/shards.css deleted file mode 100644 index 4e27ed90..00000000 --- a/src/assets/stylesheets/theme/components/shards.css +++ /dev/null @@ -1,17 +0,0 @@ -.shard-reroute-target { - background-color: transparent; - position: absolute; - height: 100%; - width: 100%; - top: 0; - left: 0; - right: 0; - bottom: 0; - border: 2px dashed grey; - color: var(--theme-muted-color) !important; -} - -.shard-reroute-target:hover { - background-color: rgba(150, 150, 150, 0.5); - cursor: pointer; -} \ No newline at end of file diff --git a/src/components/shards/ShardsTable.vue b/src/components/shards/ShardsTable.vue index 9925c284..37911278 100644 --- a/src/components/shards/ShardsTable.vue +++ b/src/components/shards/ShardsTable.vue @@ -80,6 +80,12 @@ :class="{marked: markedColumnIndex === i}" @mouseover="markColumn(i)" @mouseleave="unmarkColumn"> +
+ + {{ t('shards.shards_table.reroute.label', { node: row }) }} + +
+
- -
- -
diff --git a/src/composables/components/rest/RestQueryForm.ts b/src/composables/components/rest/RestQueryForm.ts index de2d110f..45c61795 100644 --- a/src/composables/components/rest/RestQueryForm.ts +++ b/src/composables/components/rest/RestQueryForm.ts @@ -9,6 +9,7 @@ import { fetchMethod } from '../../../helpers/fetch' import { IdbRestQueryTab, IdbRestQueryTabRequest } from '../../../db/types.ts' import { debounce } from '../../../helpers/debounce.ts' import { parseKibana } from '../../../helpers/parseKibana.ts' +import { cleanIndexName } from '../../../helpers/cleanIndexName.ts' type RestQueryFormProps = { tab: IdbRestQueryTab @@ -48,7 +49,7 @@ export const useRestQueryForm = (props: RestQueryFormProps, emit: any) => { let url = connectionStore.activeCluster.uri if (!url.endsWith('/') && !props.tab.request.path.startsWith('/')) url += '/' - url += props.tab.request.path + url += cleanIndexName(props.tab.request.path) try { const fetchResponse = await fetchMethod(url, options) @@ -143,11 +144,11 @@ export const useRestQueryForm = (props: RestQueryFormProps, emit: any) => { const onPaste = (data: string) => { const kibanaRequest = parseKibana(data) - nextTick(() => { + nextTick(() => { if (kibanaRequest.method) { ownTab.value.request.method = kibanaRequest.method - ownTab.value.request.path = kibanaRequest.path || '' - ownTab.value.request.body = kibanaRequest.body || '' + ownTab.value.request.path = kibanaRequest.path || '' + ownTab.value.request.body = kibanaRequest.body || '' } }) } diff --git a/src/helpers/cleanIndexName.ts b/src/helpers/cleanIndexName.ts new file mode 100644 index 00000000..e2cdc036 --- /dev/null +++ b/src/helpers/cleanIndexName.ts @@ -0,0 +1,14 @@ +export const cleanIndexName = (index: string) => { + return index.replace(/%/g, '%25').replace(/<.*?>/g, (match) => { + return match + .replace(//g, '%3E') + .replace(/\//g, '%2F') + .replace(/\{/g, '%7B') + .replace(/\}/g, '%7D') + .replace(/\|/g, '%7C') + .replace(/\+/g, '%2B') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C') + }) +} diff --git a/src/services/ElasticsearchAdapter.ts b/src/services/ElasticsearchAdapter.ts index c9b137b3..e9d2d03c 100644 --- a/src/services/ElasticsearchAdapter.ts +++ b/src/services/ElasticsearchAdapter.ts @@ -3,6 +3,7 @@ import { REQUEST_DEFAULT_HEADERS } from '../consts' import { fetchMethod } from '../helpers/fetch' import { stringifyJson } from '../helpers/json/stringify.ts' import { ElasticsearchClusterCredentials } from '../store/connection.ts' +import { cleanIndexName } from '../helpers/cleanIndexName.ts' interface IndexGetArgs { routing?: string @@ -131,7 +132,7 @@ export default class ElasticsearchAdapter { if (indices.length > MAX_INDICES_PER_REQUEST) { return this.callInChunks({ method: 'indexClose', indices }) } else { - return this.request(`${indices.join(',')}/_close`, 'POST') + return this.request(`${cleanIndexName(indices.join(','))}/_close`, 'POST') } } @@ -139,7 +140,7 @@ export default class ElasticsearchAdapter { if (indices.length > MAX_INDICES_PER_REQUEST) { return this.callInChunks({ method: 'indexOpen', indices }) } else { - return this.request(`${indices.join(',')}/_open`, 'POST') + return this.request(`${cleanIndexName(indices.join(','))}/_open`, 'POST') } } @@ -147,7 +148,7 @@ export default class ElasticsearchAdapter { if (indices.length > MAX_INDICES_PER_REQUEST) { return this.callInChunks({ method: 'indexForcemerge', indices }) } else { - return this.request(`${indices.join(',')}/_forcemerge`, 'POST') + return this.request(`${cleanIndexName(indices.join(','))}/_forcemerge`, 'POST') } } @@ -155,7 +156,7 @@ export default class ElasticsearchAdapter { if (indices.length > MAX_INDICES_PER_REQUEST) { return this.callInChunks({ method: 'indexRefresh', indices }) } else { - return this.request(`${indices.join(',')}/_refresh`, 'POST') + return this.request(`${cleanIndexName(indices.join(','))}/_refresh`, 'POST') } } @@ -163,7 +164,7 @@ export default class ElasticsearchAdapter { if (indices.length > MAX_INDICES_PER_REQUEST) { return this.callInChunks({ method: 'indexClearCache', indices }) } else { - return this.request(`${indices.join(',')}/_cache/clear`, 'POST') + return this.request(`${cleanIndexName(indices.join(','))}/_cache/clear`, 'POST') } } @@ -171,7 +172,7 @@ export default class ElasticsearchAdapter { if (indices.length > MAX_INDICES_PER_REQUEST) { return this.callInChunks({ method: 'indexFlush', indices }) } else { - return this.request(`${indices.join(',')}/_flush`, 'POST') + return this.request(`${cleanIndexName(indices.join(','))}/_flush`, 'POST') } } @@ -316,19 +317,4 @@ export default class ElasticsearchAdapter { return Promise.reject(e) } } - - /********/ - - /** - * Creates multiple indices, one for each word. Only creates if they do not already exist - * @param names {Array} - */ - async createIndices (names: string[]) { - for (const name of [...new Set(names)]) { - const exists = await this.indexExists({ index: name }) - if (!exists) await this.indexCreate({ index: name }) - } - } } - -const cleanIndexName = (index: string) => (index.replace(/%/g, '%25')) diff --git a/tests/unit/helpers/cleanIndexName.spec.ts b/tests/unit/helpers/cleanIndexName.spec.ts new file mode 100644 index 00000000..546e29f3 --- /dev/null +++ b/tests/unit/helpers/cleanIndexName.spec.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest' +import { cleanIndexName } from '../../../src/helpers/cleanIndexName' + +describe.concurrent('helpers/cleanIndexName.ts', () => { + it('should do nothing when no special characters are present', () => { + const indexNames = [ + '', + 'movies', + 'kube-2024.12.21' + ] + indexNames.forEach(indexName => { + expect(cleanIndexName(indexName)).toBe(indexName) + }) + }) + + it('should encode special characters inside < and >', () => { + const indexName = 'foo//_doc' + const cleanedName = 'foo/%3Ckube-%7Bnow%2Fd%7D%3E/_doc' + expect(cleanIndexName(indexName)).toBe(cleanedName) + }) + + it('should encode multiple <...> segments correctly', () => { + const indexName = 'foo//bar/' + const cleanedName = 'foo/%3Ckube-%7Bnow%2Fd%7D%3E/bar/%3Cbaz%2Bfoo%3E' + expect(cleanIndexName(indexName)).toBe(cleanedName) + }) + + it('should encode % everywhere, but other characters only inside < and >', () => { + const indexName = 'foo/%//bar' + const cleanedName = 'foo/%25/%3Ckube%25%2B%3E/bar' + expect(cleanIndexName(indexName)).toBe(cleanedName) + }) + + it('should not encode characters outside of < and >', () => { + const indexName = 'movies/kube+foo/bar' + const cleanedName = 'movies/kube+foo/bar' + expect(cleanIndexName(indexName)).toBe(cleanedName) + }) +})