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/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) + }) +})