diff --git a/apps/datahub-e2e/src/e2e/search.cy.ts b/apps/datahub-e2e/src/e2e/search.cy.ts index a653249b..a37623cb 100644 --- a/apps/datahub-e2e/src/e2e/search.cy.ts +++ b/apps/datahub-e2e/src/e2e/search.cy.ts @@ -184,7 +184,7 @@ describe('search', () => { .find('h1') .should( 'have.text', - " Concentrations annuelles de polluants dans l'air ambiant issues du réseau permanent de mesures en région Hauts-de-France " + ' Metadata for E2E testing purpose. (this title is too long and should be cut, this title is too long and should be cut, this title is too long and should be cut, this title is too long and should be cut, this title is too long and should be cut) ' ) }) it('should filter the results when selecting multiple filter values (producer)', () => { @@ -193,7 +193,7 @@ describe('search', () => { cy.get('@options').first().click() cy.get('@options').eq(1).click() cy.get('@options').eq(2).click() - cy.get('mel-datahub-results-card-search').should('have.length', 3) + cy.get('mel-datahub-results-card-search').should('have.length', 5) }) it('should filter by quality score', () => { cy.get('[data-cy="filterExpandBtn"]').click() @@ -222,12 +222,15 @@ describe('search', () => { cy.get('@result-cards').should('have.length', 3) cy.get('@filters').eq(1).click() getFilterOptions() - cy.get('@options').eq(12).click() + cy.get('@options').eq(5).click() cy.get('@result-cards').should('have.length', 1) cy.get('@result-cards') .first() .find('h1') - .should('have.text', ' Accroches vélos MEL ') + .should( + 'have.text', + ' SCoT (Schéma de cohérence territoriale) en région Hauts-de-France ' + ) }) it('should combine search input and filters and display a message if no results found', () => { cy.get('mel-datahub-autocomplete input').type('test') @@ -237,7 +240,7 @@ describe('search', () => { cy.get('@result-cards').should('have.length', 3) cy.get('@filters').eq(1).click() getFilterOptions() - cy.get('@options').eq(10).click() + cy.get('@options').eq(8).click() cy.get('[data-cy=searchResults]').should( 'have.text', ' Aucune correspondance. ' @@ -257,7 +260,7 @@ describe('search', () => { cy.get('@filters').eq(3).click() getFilterOptions() cy.get('@options').eq(1).click() - cy.get('@result-cards').should('have.length', 2) + cy.get('@result-cards').should('have.length', 9) cy.get('body').click() cy.get('[data-cy=filterResetBtn]').click() cy.get('@result-cards').should('have.length', 14) @@ -269,6 +272,40 @@ describe('search', () => { cy.get('mel-datahub-filter-dropdown').should('have.length', 3) }) }) + describe.only('Filters from config', () => { + beforeEach(() => { + // this will enable all available filters + cy.intercept('GET', '/assets/configuration/default.toml', { + fixture: 'config-with-all-filters.toml', + }) + cy.visit('/search') + }) + it('should display all filters', () => { + cy.get('[data-cy="filterExpandBtn"]').click() + cy.get('@filters').filter(':visible').should('have.length', 12) + cy.get('@filters') + .children() + .then(($dropdowns) => + $dropdowns + .toArray() + .map((dropdown) => dropdown.getAttribute('data-cy-field')) + ) + .should('eql', [ + 'publisher', + 'format', + 'publicationYear', + 'inspireKeyword', + 'keyword', + 'topic', + 'isSpatial', + 'license', + 'resourceType', + 'representationType', + 'revisionYear', + 'categoryKeyword', + ]) + }) + }) }) describe('pagination', () => { beforeEach(() => { diff --git a/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml b/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml new file mode 100644 index 00000000..bfa84e76 --- /dev/null +++ b/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml @@ -0,0 +1,2 @@ +[search] +advanced_filters = ['publisher', 'format', 'publicationYear', 'inspireKeyword', 'keyword', 'topic', 'isSpatial', 'license', 'resourceType', 'representationType', 'revisionYear', 'categoryKeyword'] diff --git a/apps/datahub/project.json b/apps/datahub/project.json index 98d2fba1..60612c14 100644 --- a/apps/datahub/project.json +++ b/apps/datahub/project.json @@ -21,6 +21,11 @@ "glob": "**/*", "input": "resources/assets", "output": "./assets" + }, + { + "glob": "*", + "input": "conf", + "output": "assets/configuration/" } ], "styles": ["resources/styles.css"], diff --git a/apps/datahub/src/app/search/search-filters/search-filters.component.ts b/apps/datahub/src/app/search/search-filters/search-filters.component.ts index ec383d27..c28c2108 100644 --- a/apps/datahub/src/app/search/search-filters/search-filters.component.ts +++ b/apps/datahub/src/app/search/search-filters/search-filters.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { marker } from '@biesbjerg/ngx-translate-extract-marker' import { RouterFacade } from 'geonetwork-ui' +import { getOptionalSearchConfig } from '@mel-dataplatform/mel' marker('mel.datahub.search.filters.topic') marker('mel.datahub.search.filters.categoryKeyword') @@ -11,6 +12,12 @@ marker('mel.datahub.search.filters.qualityScore') marker('mel.datahub.search.filters.territories') marker('mel.datahub.search.filters.producerOrg') marker('mel.datahub.search.filters.publisherOrg') +marker('mel.datahub.search.filters.format') +marker('mel.datahub.search.filters.inspireKeyword') +marker('mel.datahub.search.filters.keyword') +marker('mel.datahub.search.filters.isSpatial') +marker('mel.datahub.search.filters.resourceType') +marker('mel.datahub.search.filters.representationType') @Component({ selector: 'mel-datahub-search-filters', @@ -21,14 +28,16 @@ marker('mel.datahub.search.filters.publisherOrg') export class SearchFiltersComponent { constructor(private routerFacade: RouterFacade) {} displayCount = 3 - searchConfig = [ - 'categoryKeyword', - 'organization', - 'publicationYear', - 'license', - 'qualityScore', - 'territories', - ].map((filter) => ({ + searchConfig = ( + getOptionalSearchConfig().ADVANCED_FILTERS || [ + 'categoryKeyword', + 'organization', + 'revisionYear', + 'license', + 'qualityScore', + 'territories', + ] + ).map((filter) => ({ fieldName: filter, title: `mel.datahub.search.filters.${filter}`, })) diff --git a/apps/datahub/src/index.html b/apps/datahub/src/index.html index 00afae57..3c519cd7 100644 --- a/apps/datahub/src/index.html +++ b/apps/datahub/src/index.html @@ -7,6 +7,12 @@ + diff --git a/apps/datahub/src/main.ts b/apps/datahub/src/main.ts index 16de2365..04a8ca21 100644 --- a/apps/datahub/src/main.ts +++ b/apps/datahub/src/main.ts @@ -1,6 +1,9 @@ -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppModule } from './app/app.module'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' +import { AppModule } from './app/app.module' +import { loadAppConfig } from '@mel-dataplatform/mel' -platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch((err) => console.error(err)); +loadAppConfig().then(() => { + platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)) +}) diff --git a/conf/default.toml b/conf/default.toml new file mode 100644 index 00000000..a1f0ba52 --- /dev/null +++ b/conf/default.toml @@ -0,0 +1,12 @@ +# MEL configuration +# Note: this file's syntax is TOML (https://toml.io/) + +### SEARCH SETTINGS + +# This section contains settings used for fine-tuning the search experience +[search] + +# The advanced search filters available to the user can be customized with this setting. +# The following fields can be used for filtering: 'organization', 'format', 'publicationYear', 'inspireKeyword', 'keyword', 'topic', 'isSpatial', 'license', 'resourceType', 'representationType', 'revisionYear', 'categoryKeyword', 'qualityScore', 'territories', 'publisherOrg', 'producerOrg' +# any other field will be ignored +# advanced_filters = ['format', 'topic', 'keyword', 'organization', 'publisherOrg', 'producerOrg'] diff --git a/libs/mel/src/index.ts b/libs/mel/src/index.ts index e56c4a05..6bcea462 100644 --- a/libs/mel/src/index.ts +++ b/libs/mel/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/mel.module' export * from './lib/embedded.translate.loader' export * from './lib/route.utils' +export * from './lib/util/app-config' diff --git a/libs/mel/src/lib/util/app-config.ts b/libs/mel/src/lib/util/app-config.ts new file mode 100644 index 00000000..a1abf79e --- /dev/null +++ b/libs/mel/src/lib/util/app-config.ts @@ -0,0 +1,61 @@ +import * as TOML from '@ltd/j-toml' +import { parseConfigSection } from './parse-utils' +import { SearchConfig } from './model' + +let searchConfig: SearchConfig | null = null + +export function getOptionalSearchConfig(): SearchConfig | null { + return searchConfig +} + +let appConfigLoaded = false + +export function loadAppConfig() { + return fetch('assets/configuration/default.toml') + .then((resp) => { + if (!resp.ok) throw new Error('Configuration file could not be loaded') + return resp.text() + }) + .then((conf) => { + let parsed + try { + parsed = TOML.parse(conf, { joiner: '\n', bigint: false }) + } catch (e: unknown) { + throw new Error( + `An error occurred when parsing the configuration file: ${ + (e as Error).message + }` + ) + } + const errors = [] + const warnings = [] + + const parsedSearchSection = parseConfigSection( + parsed, + 'search', + [], + ['advanced_filters'], + warnings, + errors + ) + searchConfig = + parsedSearchSection === null + ? null + : ({ + ADVANCED_FILTERS: parsedSearchSection['advanced_filters'], + } as SearchConfig) + if (errors.length) { + throw new Error(`One or more mandatory settings were missing from the configuration file. + ${errors.join('\n')}`) + } else if (warnings.length) { + console.warn(`One or more unexpected settings were encountered in the configuration file. + ${warnings.join('\n')}`) + } + + appConfigLoaded = true + }) +} + +export function isConfigLoaded() { + return appConfigLoaded +} diff --git a/libs/mel/src/lib/util/model.ts b/libs/mel/src/lib/util/model.ts new file mode 100644 index 00000000..b62456ba --- /dev/null +++ b/libs/mel/src/lib/util/model.ts @@ -0,0 +1,8 @@ +import { SearchPreset } from 'geonetwork-ui' + +export interface SearchConfig { + FILTER_GEOMETRY_URL?: string + FILTER_GEOMETRY_DATA?: string + SEARCH_PRESET?: SearchPreset[] + ADVANCED_FILTERS?: [] +} diff --git a/libs/mel/src/lib/util/parse-utils.ts b/libs/mel/src/lib/util/parse-utils.ts new file mode 100644 index 00000000..6859f8ad --- /dev/null +++ b/libs/mel/src/lib/util/parse-utils.ts @@ -0,0 +1,48 @@ +export function parseConfigSection( + fullConfigObj: Record>, + sectionName: string, + mandatoryKeys: string[], + optionalKeys: string[], + outWarnings: string[], + outErrors: string[] +): Record | null { + if (typeof fullConfigObj[sectionName] !== 'object') { + if (mandatoryKeys.length === 0) return null + outErrors.push(`The [${sectionName}] mandatory section is missing.`) + return null + } + + const sectionConf = fullConfigObj[sectionName] as Record + const keysCheck = checkKeys(sectionConf, mandatoryKeys, optionalKeys) + + if (keysCheck.missing.length) { + // note: this is not thrown to allow merging several Errors down the line + outErrors.push( + `In the [${sectionName}] section: ${keysCheck.missing.join(', ')}` + ) + return null + } else if (keysCheck.unrecognized.length) { + outWarnings.push( + `In the [${sectionName}] section: ${keysCheck.unrecognized.join(', ')}` + ) + keysCheck.unrecognized.forEach((key) => delete sectionConf[key]) + } + + return sectionConf +} + +const checkKeys = ( + input: Record, + mandatory: string[], + optional: string[] +) => { + const keys = Object.keys(input) + const missing = mandatory.filter((key) => keys.indexOf(key) === -1) + const unrecognized = keys.filter( + (key) => mandatory.indexOf(key) === -1 && optional.indexOf(key) === -1 + ) + return { + missing, + unrecognized, + } +} diff --git a/resources/translations/en_MEL.json b/resources/translations/en_MEL.json index eb74eb6c..3fd181f4 100644 --- a/resources/translations/en_MEL.json +++ b/resources/translations/en_MEL.json @@ -11,13 +11,16 @@ "mel.datahub.multiselect.filter.placeholder": "", "mel.datahub.search.clear": "", "mel.datahub.search.filters.categoryKeyword": "", + "mel.datahub.search.filters.format": "", + "mel.datahub.search.filters.inspireKeyword": "", + "mel.datahub.search.filters.isSpatial": "", + "mel.datahub.search.filters.keyword": "", "mel.datahub.search.filters.license": "", "mel.datahub.search.filters.maxValue": "", "mel.datahub.search.filters.minValue": "", "mel.datahub.search.filters.more": "", "mel.datahub.search.filters.organization": "", "mel.datahub.search.filters.producerOrg": "", - "mel.datahub.search.filters.publicationYear": "", "mel.datahub.search.filters.publisherOrg": "", "mel.datahub.search.filters.qualityScore": "", "mel.datahub.search.filters.range.from": "", @@ -25,8 +28,13 @@ "mel.datahub.search.filters.reduce": "", "mel.datahub.search.filters.reset": "", "mel.datahub.search.filters.territories": "", - "mel.datahub.search.filters.topic": "", "mel.datahub.search.filters.validate": "", + "mel.datahub.search.filters.publicationYear": "", + "mel.datahub.search.filters.publisher": "", + "mel.datahub.search.filters.representationType": "", + "mel.datahub.search.filters.resourceType": "", + "mel.datahub.search.filters.revisionYear": "", + "mel.datahub.search.filters.topic": "", "mel.datahub.search.form.description": "", "mel.datahub.search.form.title": "", "mel.datahub.search.hits.found": "", diff --git a/resources/translations/fr_MEL.json b/resources/translations/fr_MEL.json index 38a6d7c1..96e9cf0b 100644 --- a/resources/translations/fr_MEL.json +++ b/resources/translations/fr_MEL.json @@ -11,13 +11,16 @@ "mel.datahub.multiselect.filter.placeholder": "Rechercher", "mel.datahub.search.clear": "Effacer", "mel.datahub.search.filters.categoryKeyword": "Thématique", + "mel.datahub.search.filters.format": "Format", + "mel.datahub.search.filters.inspireKeyword": "Mot-clé INSPIRE", + "mel.datahub.search.filters.isSpatial": "Données spatiales", + "mel.datahub.search.filters.keyword": "Mot-clé", "mel.datahub.search.filters.license": "Licence", "mel.datahub.search.filters.maxValue": "Valeur maximale", "mel.datahub.search.filters.minValue": "Valeur minimale", "mel.datahub.search.filters.more": "Plus de filtres", "mel.datahub.search.filters.organization": "Organisation", "mel.datahub.search.filters.producerOrg": "Producteur", - "mel.datahub.search.filters.publicationYear": "Date", "mel.datahub.search.filters.publisherOrg": "Distributeur", "mel.datahub.search.filters.qualityScore": "Score de qualité", "mel.datahub.search.filters.range.from": "De :", @@ -27,6 +30,11 @@ "mel.datahub.search.filters.territories": "Territoires", "mel.datahub.search.filters.topic": "Catégories", "mel.datahub.search.filters.validate": "Valider", + "mel.datahub.search.filters.publicationYear": "Année de publication", + "mel.datahub.search.filters.publisher": "Producteur", + "mel.datahub.search.filters.representationType": "Type de représentation", + "mel.datahub.search.filters.resourceType": "Type de ressource", + "mel.datahub.search.filters.revisionYear": "Date", "mel.datahub.search.form.description": "Vous pouvez utiliser la barre de recherche ou les différents filtres situés ci-dessous pour trouver un jeu de données plus rapidement.", "mel.datahub.search.form.title": "Trouver un jeu de données", "mel.datahub.search.hits.found": "{hits, plural, =0{Aucune correspondance.} one{1 enregistrement trouvé.} other{Ensemble des données: {hits}}}", diff --git a/tools/docker/docker-entrypoint.sh b/tools/docker/docker-entrypoint.sh index 93a58f7a..147bae9e 100755 --- a/tools/docker/docker-entrypoint.sh +++ b/tools/docker/docker-entrypoint.sh @@ -1,5 +1,35 @@ #!/bin/bash +APP_FILES_PATH=/usr/share/nginx/html/${APP_NAME}/ + +CONFIG_FILE_PATH=assets/configuration/ +CONFIG_FILE_NAME=default.toml +CONFIG_OVERRIDE_FILE_PATH=${CONFIG_DIRECTORY_OVERRIDE:-/conf}/${CONFIG_FILE_NAME} + +## 1. COPY CONFIG FILE + +# check if conf file from $CONFIG_DIRECTORY_OVERRIDE (defaults to /conf) is present +if [ -f "${CONFIG_OVERRIDE_FILE_PATH}" ] +then + # copy file straight to the assets + echo "[INFO] Copying custom configuration file located at ${CONFIG_OVERRIDE_FILE_PATH}..." + cp ${CONFIG_OVERRIDE_FILE_PATH} ${APP_FILES_PATH}${CONFIG_FILE_PATH}${CONFIG_FILE_NAME} +else + # no conf file; use env variables to tweak app config + echo "[INFO] No custom configuration file found at ${CONFIG_OVERRIDE_FILE_PATH}" + # Modify the GN4 url and proxy path based on env variables (if defined) + if [ ! -z "${GN4_API_URL}" ] + then + echo "[INFO] Replacing GN4 url in conf with: ${GN4_API_URL}..." + sed -i "s%geonetwork4_api_url = \".*\"%geonetwork4_api_url = \"${GN4_API_URL}\"%" ${APP_FILES_PATH}${CONFIG_FILE_PATH}${CONFIG_FILE_NAME} + fi + if [ ! -z "${PROXY_PATH}" ] + then + echo "[INFO] Replacing proxy path in conf with: ${PROXY_PATH}..." + sed -i "s%proxy_path = \".*\"%proxy_path = \"${PROXY_PATH}\"%" ${APP_FILES_PATH}${CONFIG_FILE_PATH}${CONFIG_FILE_NAME} + fi +fi + # Executing custom scripts located in CUSTOM_SCRIPTS_DIRECTORY if environment variable is set if [[ -z "${CUSTOM_SCRIPTS_DIRECTORY}" ]]; then echo "[INFO] No CUSTOM_SCRIPTS_DIRECTORY env variable set"