From 4c051585b62977d84ff26526f9d473c7a04b83c6 Mon Sep 17 00:00:00 2001 From: Mercy Date: Thu, 20 Feb 2025 10:12:57 -0500 Subject: [PATCH 1/4] Update of translations from socket events (#7691) * ensures update of translate components * update expected text * removes unused variable * SET at action name * eslint * cleaning --- app/react/App/sockets.js | 6 +- app/react/I18N/specs/fixtures.ts | 18 +--- .../I18N/specs/translateFunction.spec.tsx | 88 ++++++++++++++----- app/react/I18N/translateFunction.tsx | 14 --- app/react/V2/atoms/store.ts | 2 +- 5 files changed, 70 insertions(+), 58 deletions(-) diff --git a/app/react/App/sockets.js b/app/react/App/sockets.js index af401ebbe7..c207aae489 100644 --- a/app/react/App/sockets.js +++ b/app/react/App/sockets.js @@ -69,7 +69,7 @@ socket.on('translationsChange', languageTranslations => { } else { translations.push(languageTranslations); } - atomStore.set(translationsAtom, translations); + atomStore.set(translationsAtom, [...translations]); }); socket.on('translationKeysChange', translationsEntries => { @@ -80,7 +80,7 @@ socket.on('translationKeysChange', translationsEntries => { .contexts.find(c => c.id && c.id === item.context.id); modifiedContext.values[item.key] = item.value; }); - atomStore.set(translationsAtom, translations); + atomStore.set(translationsAtom, [...translations]); }); socket.on('translationsInstallDone', () => { @@ -109,7 +109,7 @@ socket.on('translationsInstallError', errorMessage => { socket.on('translationsDelete', locale => { const translations = atomStore.get(translationsAtom); const updatedTranslations = translations.filter(language => language.locale !== locale); - atomStore.set(translationsAtom, updatedTranslations); + atomStore.set(translationsAtom, [...updatedTranslations]); }); socket.on('translationsDeleteDone', () => { diff --git a/app/react/I18N/specs/fixtures.ts b/app/react/I18N/specs/fixtures.ts index 8e7fc17029..44dd7978e8 100644 --- a/app/react/I18N/specs/fixtures.ts +++ b/app/react/I18N/specs/fixtures.ts @@ -45,20 +45,4 @@ const translations: ClientTranslationSchema[] = [ }, ]; -const updatedTranslations: ClientTranslationSchema[] = [ - translations[0], - { - ...translations[1], - contexts: [ - { - ...translations[1].contexts, - values: { - Search: 'Buscar', - confirmDeleteDocument: 'Actualizado!', - }, - }, - ], - }, -]; - -export { translations, updatedTranslations, languages }; +export { translations, languages }; diff --git a/app/react/I18N/specs/translateFunction.spec.tsx b/app/react/I18N/specs/translateFunction.spec.tsx index 560b1f3206..0e18c9518f 100644 --- a/app/react/I18N/specs/translateFunction.spec.tsx +++ b/app/react/I18N/specs/translateFunction.spec.tsx @@ -2,11 +2,13 @@ * @jest-environment jsdom */ import React from 'react'; -import { act, render, RenderResult } from '@testing-library/react'; import { Provider } from 'jotai'; +import { act, render, RenderResult } from '@testing-library/react'; import { localeAtom, translationsAtom, atomStore } from 'V2/atoms'; +import { socket } from 'app/socket'; +import 'app/App/sockets'; import { t } from '../translateFunction'; -import { translations, updatedTranslations } from './fixtures'; +import { translations } from './fixtures'; describe('t function', () => { let renderResult: RenderResult; @@ -19,7 +21,6 @@ describe('t function', () => { beforeEach(() => { atomStore.set(translationsAtom, translations); atomStore.set(localeAtom, locale); - jest.spyOn(atomStore, 'sub'); locale = 'es'; }); @@ -32,12 +33,10 @@ describe('t function', () => { expect( renderResult.getByText('¿Esta seguro que quiere borrar este documento?') ).toBeInTheDocument(); - expect(atomStore.sub).toHaveBeenCalledTimes(3); - expect(t.translation).toBe(undefined); }); describe('no component', () => { - it('should return the translated string and subscribe to the atom store', () => { + it('should return the translated string', () => { renderEnvironment( 'System', 'confirmDeleteDocument', @@ -47,32 +46,75 @@ describe('t function', () => { expect( renderResult.getByText('¿Esta seguro que quiere borrar este documento?') ).toBeInTheDocument(); - // expect(atomStore.sub).toHaveBeenCalledTimes(4); - // expect(t.translation).toEqual({ - // contexts: translations[1].contexts, - // locale: 'es', - // }); }); - it('should update translation when the atom updates', async () => { - renderEnvironment( - 'System', - 'confirmDeleteDocument', - 'Are you sure you want to delete this document?', - false + it('should update translation when the atom is updated partially from the socket', async () => { + const result = render( + + {t( + 'System', + 'confirmDeleteDocument', + 'Are you sure you want to delete this document?', + true + )} + ); + expect( - renderResult.getByText('¿Esta seguro que quiere borrar este documento?') + result.getByText('¿Esta seguro que quiere borrar este documento?') ).toBeInTheDocument(); + const translation = { + locale: 'es', + contexts: [ + { + id: 'System', + label: 'System', + values: { + Search: 'Buscar', + confirmDeleteDocument: '¿CONFIRMA ELIMINACION?', + }, + }, + ], + }; + await act(async () => { - atomStore.set(translationsAtom, updatedTranslations); + //@ts-ignore accessing internal _callbacks for testing purposes + socket._callbacks.$translationsChange[0](translation); }); - // expect(t.translation).toEqual({ - // contexts: updatedTranslations[1].contexts, - // locale: 'es', - // }); + await act(async () => { + expect(result.getByText('¿CONFIRMA ELIMINACION?')).toBeInTheDocument(); + }); + }); + + it('should update translation when the atom is updated fully from the socket', async () => { + const result = render( + {t('System', 'Search', 'Search', true)} + ); + + expect(result.getByText('Buscar')).toBeInTheDocument(); + + const translationKeysChangeArguments = [ + { + language: 'es', + value: 'Busqueda', + key: 'Search', + context: { + id: 'System', + label: 'System', + }, + }, + ]; + + await act(async () => { + //@ts-ignore accessing internal _callbacks for testing purposes + socket._callbacks.$translationKeysChange[0](translationKeysChangeArguments); + }); + + await act(async () => { + expect(result.getByText('Busqueda')).toBeInTheDocument(); + }); }); }); diff --git a/app/react/I18N/translateFunction.tsx b/app/react/I18N/translateFunction.tsx index ef74ad3948..54b62414eb 100644 --- a/app/react/I18N/translateFunction.tsx +++ b/app/react/I18N/translateFunction.tsx @@ -6,20 +6,8 @@ import { Translate } from './Translate'; //return type as any since there is no way to create conditional returns based on parameters interface TranslationFunction { (contextId?: string, key?: string, text?: string | null, returnComponent?: boolean): any; - translation?: string; } -// const updateTranslations = () => { -// const translations = atomStore.get(translationsAtom); -// const locale = atomStore.get(localeAtom); -// t.translation = getLocaleTranslation(translations, locale); -// return { translations, locale }; -// }; -// -// atomStore.sub(translationsAtom, () => { -// updateTranslations(); -// }); - const t: TranslationFunction = (contextId, key, text, returnComponent = true) => { if (!contextId) { // eslint-disable-next-line no-console @@ -30,8 +18,6 @@ const t: TranslationFunction = (contextId, key, text, returnComponent = true) => return {key}; } - // updateTranslations(); - const translations = atomStore.get(translationsAtom); const locale = atomStore.get(localeAtom); const context = getContext(getLocaleTranslation(translations, locale), contextId); diff --git a/app/react/V2/atoms/store.ts b/app/react/V2/atoms/store.ts index 2a728e8c65..26ce3f1c8a 100644 --- a/app/react/V2/atoms/store.ts +++ b/app/react/V2/atoms/store.ts @@ -69,7 +69,7 @@ if (isClient && window.__atomStoreData__) { }); atomStore.sub(translationsAtom, () => { const value = atomStore.get(translationsAtom); - store?.dispatch({ type: 'translations', value }); + store?.dispatch({ type: 'translations/SET', value }); }); } From 4842c79cfc22e91d88ce0bdf49f577ac4f8c7dcd Mon Sep 17 00:00:00 2001 From: Mercy Date: Mon, 24 Feb 2025 11:42:29 -0500 Subject: [PATCH 2/4] translate templates name in selector (#7702) --- app/react/Metadata/components/MetadataForm.js | 20 ++++++++++++++++++- .../components/specs/MetadataForm.spec.js | 7 +++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/react/Metadata/components/MetadataForm.js b/app/react/Metadata/components/MetadataForm.js index 5a6988593e..0674be4d17 100644 --- a/app/react/Metadata/components/MetadataForm.js +++ b/app/react/Metadata/components/MetadataForm.js @@ -22,9 +22,27 @@ import { PDFUpload } from './PDFUpload'; import { DeleteSelectionButton } from './DeleteSelectionButton'; const immutableDefaultTemplate = Immutable.fromJS(defaultTemplate); + const selectTemplateOptions = createSelector( s => s.templates, - templates => templates.map(tmpl => ({ label: tmpl.get('name'), value: tmpl.get('_id') })) + s => s.translations, + s => s.locale, + (templates, translations, locale) => { + const translationContexts = translations.find( + translation => translation.get('locale') === locale + ); + return templates.map(tmpl => { + const [translationContext] = translationContexts + .get('contexts') + .filter(context => context.get('id') === tmpl.get('_id')); + + const label = translationContext + ? t(tmpl.get('_id'), tmpl.get('name'), null, false) + : tmpl.get('name'); + + return { label, value: tmpl.get('_id') }; + }); + } ); class MetadataForm extends Component { diff --git a/app/react/Metadata/components/specs/MetadataForm.spec.js b/app/react/Metadata/components/specs/MetadataForm.spec.js index e52f305368..de888ab03f 100644 --- a/app/react/Metadata/components/specs/MetadataForm.spec.js +++ b/app/react/Metadata/components/specs/MetadataForm.spec.js @@ -214,6 +214,13 @@ describe('MetadataForm', () => { templates, metadata: { attachments: [], sharedId: 'entitySharedId' }, attachments: { progress: Immutable.fromJS({}) }, + translations: Immutable.fromJS([ + { + locale: 'en', + contexts: [{ _id: 'template1', values: { Title: 'Title translated' } }], + }, + ]), + locale: 'en', }; ownProps = { templates, templateId: templates.get(1).get('_id'), model: 'metadata' }; }); From f3a00e5e760419e7f54850382f98dc7f90fbfa29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20P=C3=B3lit?= Date: Mon, 24 Feb 2025 12:35:22 -0500 Subject: [PATCH 3/4] Configured elastic index to have replicas configurable by tenant (#7676) * Configured elastic index to have replicas configurable by tenant * Changed esUseReplicas to esReplicas: number * Added flag for REINDEX_WITH_OPTIMIZATION * Renamed wrongly named feature * Upped version --- app/api/config.ts | 1 + app/api/search/entitiesIndex.js | 8 ++-- app/api/tenants/specs/tenantESMapping.spec.ts | 29 ++++++++++++++ app/api/tenants/specs/tenantsModel.spec.ts | 2 + app/api/tenants/tenantContext.ts | 1 + app/api/tenants/tenantESMapping.ts | 16 ++++++++ app/api/tenants/tenantsModel.ts | 1 + database/reindex_elastic.js | 40 +++++++++++++------ package.json | 2 +- 9 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 app/api/tenants/specs/tenantESMapping.spec.ts create mode 100644 app/api/tenants/tenantESMapping.ts diff --git a/app/api/config.ts b/app/api/config.ts index 4131b97a7a..6f3c8756de 100644 --- a/app/api/config.ts +++ b/app/api/config.ts @@ -68,6 +68,7 @@ export const config = { activityLogs: ACTIVITY_LOGS_FOLDER || `${filesRootPath}/log/`, featureFlags: { s3Storage: false, + esReplicas: 0, }, }, externalServices: Boolean(process.env.EXTERNAL_SERVICES) || false, diff --git a/app/api/search/entitiesIndex.js b/app/api/search/entitiesIndex.js index 61136929b7..207e7eeb44 100644 --- a/app/api/search/entitiesIndex.js +++ b/app/api/search/entitiesIndex.js @@ -10,11 +10,11 @@ import { MongoSettingsDataSource } from 'api/settings.v2/database/MongoSettingsD import { LanguageUtils } from 'shared/language'; import { DefaultTransactionManager } from 'api/common.v2/database/data_source_defaults'; import { otherLanguageSchema } from 'shared/language/availableLanguages'; -import elasticMapping from '../../../database/elastic_mapping/elastic_mapping'; +import { getTenantESMapping } from 'api/tenants/tenantESMapping'; import elasticMapFactory from '../../../database/elastic_mapping/elasticMapFactory'; import { elastic } from './elastic'; -export class IndexError extends Error {} +class IndexError extends Error {} const preprocessEntitiesToIndex = async entitiesToIndex => { const db = getConnection(); @@ -183,9 +183,9 @@ const updateMapping = async tmpls => { const reindexAll = async (tmpls, searchInstance) => { await elastic.indices.delete(); - await elastic.indices.create({ body: elasticMapping }); + await elastic.indices.create({ body: getTenantESMapping() }); await updateMapping(tmpls); return indexEntities({ query: {}, searchInstance }); }; -export { bulkIndex, indexEntities, updateMapping, reindexAll }; +export { IndexError, bulkIndex, indexEntities, updateMapping, reindexAll }; diff --git a/app/api/tenants/specs/tenantESMapping.spec.ts b/app/api/tenants/specs/tenantESMapping.spec.ts new file mode 100644 index 0000000000..c823967fb7 --- /dev/null +++ b/app/api/tenants/specs/tenantESMapping.spec.ts @@ -0,0 +1,29 @@ +import { tenants } from '../tenantContext'; +import { getTenantESMapping } from '../tenantESMapping'; + +describe('tenantESMapping', () => { + describe('getTenantESMapping', () => { + it('should use the base elastic mapping', async () => { + tenants.add({ + name: 'test-tenant', + dbName: 'test-tenant-db', + }); + + await tenants.run(async () => { + expect(getTenantESMapping().settings['index.number_of_replicas']).toBe(0); + }, 'test-tenant'); + }); + + it('should use the append tenant specific configuration to base mapping', async () => { + tenants.add({ + name: 'test-tenant', + dbName: 'test-tenant-db', + featureFlags: { esReplicas: 2 }, + }); + + await tenants.run(async () => { + expect(getTenantESMapping().settings['index.number_of_replicas']).toBe(2); + }, 'test-tenant'); + }); + }); +}); diff --git a/app/api/tenants/specs/tenantsModel.spec.ts b/app/api/tenants/specs/tenantsModel.spec.ts index 84d94ee36b..9476ff2315 100644 --- a/app/api/tenants/specs/tenantsModel.spec.ts +++ b/app/api/tenants/specs/tenantsModel.spec.ts @@ -51,6 +51,7 @@ describe('tenantsModel', () => { healthChecks: 'un-needed data', featureFlags: { s3Storage: false, + esReplicas: 1, }, }, { @@ -82,6 +83,7 @@ describe('tenantsModel', () => { activityLogs: 'path', featureFlags: { s3Storage: false, + esReplicas: 1, }, }); expect(tenantTwo).toEqual({ diff --git a/app/api/tenants/tenantContext.ts b/app/api/tenants/tenantContext.ts index 619b8146e7..97e8fd40b4 100644 --- a/app/api/tenants/tenantContext.ts +++ b/app/api/tenants/tenantContext.ts @@ -15,6 +15,7 @@ type Tenant = { activityLogs: string; featureFlags?: { s3Storage?: boolean; + esReplicas?: number; sync?: boolean; v1_transactions?: boolean; }; diff --git a/app/api/tenants/tenantESMapping.ts b/app/api/tenants/tenantESMapping.ts new file mode 100644 index 0000000000..add63685d4 --- /dev/null +++ b/app/api/tenants/tenantESMapping.ts @@ -0,0 +1,16 @@ +import { tenants } from './index'; +import elasticMapping from '../../../database/elastic_mapping/elastic_mapping'; + +const getTenantESMapping = () => { + const tenantElasticMapping = { + settings: { ...elasticMapping.settings }, + mappings: { ...elasticMapping.mappings }, + }; + + tenantElasticMapping.settings['index.number_of_replicas'] = + tenants.current().featureFlags?.esReplicas || 0; + + return tenantElasticMapping; +}; + +export { getTenantESMapping }; diff --git a/app/api/tenants/tenantsModel.ts b/app/api/tenants/tenantsModel.ts index a6e15c36ed..5ec727ac5b 100644 --- a/app/api/tenants/tenantsModel.ts +++ b/app/api/tenants/tenantsModel.ts @@ -30,6 +30,7 @@ const mongoSchema = new mongoose.Schema({ activityLogs: String, featureFlags: { s3Storage: Boolean, + esReplicas: Number, sync: Boolean, v1_transactions: Boolean, }, diff --git a/database/reindex_elastic.js b/database/reindex_elastic.js index f048587097..7dc8e7939e 100644 --- a/database/reindex_elastic.js +++ b/database/reindex_elastic.js @@ -21,24 +21,40 @@ const headers = { 'Content-Type': 'application/json', }; -const setReindexSettings = async (refreshInterval, numberOfReplicas, translogDurability) => - fetch(`${getIndexUrl()}/_settings`, { - method: 'PUT', - headers, - body: { - index: { - refresh_interval: refreshInterval, - number_of_replicas: numberOfReplicas, - translog: { - durability: translogDurability, - }, +const setReindexSettings = async (refreshInterval, numberOfReplicas, translogDurability) => { + let body = { + index: { + refresh_interval: refreshInterval, + number_of_replicas: numberOfReplicas, + translog: { + durability: translogDurability, }, }, + }; + + if (process.env.REINDEX_WITH_OPTIMIZATION) { + body = JSON.stringify(body); + } + + const result = await fetch(`${getIndexUrl()}/_settings`, { + method: 'PUT', + headers, + body, }); + return result; +}; + const restoreSettings = async () => { process.stdout.write('Restoring index settings...'); - const result = setReindexSettings('1s', 0, 'request'); + + const tenantReplicas = tenants.current().featureFlags?.esReplicas || 0; + + if (tenants.current().featureFlags?.esReplicas) { + process.stdout.write('restoring ES Replicas...'); + } + + const result = setReindexSettings('1s', tenantReplicas, 'request'); process.stdout.write(' [done]\n'); return result; }; diff --git a/package.json b/package.json index 080e683be6..197b5685ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uwazi", - "version": "1.201.0", + "version": "1.201.1", "description": "Uwazi is a free, open-source solution for organising, analysing and publishing your documents.", "keywords": [ "react" From 7814befd00379093fb54ebfdb55ea2b008065d8e Mon Sep 17 00:00:00 2001 From: A happy cat Date: Tue, 25 Feb 2025 03:51:01 +0100 Subject: [PATCH 4/4] rendering nothing when there is no label nor url --- app/react/Metadata/components/Metadata.js | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/react/Metadata/components/Metadata.js b/app/react/Metadata/components/Metadata.js index b3ac0f6fc1..631cce44cd 100644 --- a/app/react/Metadata/components/Metadata.js +++ b/app/react/Metadata/components/Metadata.js @@ -40,6 +40,10 @@ const renderRelationshipLinks = (linksProp, compact) => { const renderLink = (prop, compact) => { const { url, label } = prop.value; const renderLabel = label || url; + if (!renderLabel) { + return null; + } + return ( {renderLabel.length > 40 && compact ? `${renderLabel.substring(0, 40)}...` : renderLabel} diff --git a/package.json b/package.json index 197b5685ff..4d125078ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uwazi", - "version": "1.201.1", + "version": "1.201.2", "description": "Uwazi is a free, open-source solution for organising, analysing and publishing your documents.", "keywords": [ "react"