From f4d2f0b782075efe6d71a70f63b2416ab3e98b32 Mon Sep 17 00:00:00 2001 From: Imen Chermiti <127976664+ImenOuidou@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:19:10 +0200 Subject: [PATCH 1/3] feat: Sort search result (#351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add pgadmin * chore: fix key * chore: missing keys * chore: typo * chore: numeric user * chore: secret * chore: containerPort * chore: auth * chore: probespath * feat: add sort search result * fix order search result * fix order search result * fix order search result * fix order search result * fix order search result --------- Co-authored-by: Gary van Woerkens Co-authored-by: Matéo Mévollon --- src/client/src/components/Search/Search.js | 8 ++- .../SearchAwesomeTable/SearchAwesomeTable.js | 32 ++++++++++- .../components/SearchResults/SearchResults.js | 14 ++--- src/client/src/containers/Search/Search.js | 38 ++++++++++--- .../src/services/Store/actions/search.js | 18 +++++++ .../services/Store/constants/ActionTypes.js | 3 ++ src/client/src/services/Store/hooks/search.js | 21 +++++++- .../src/services/Store/reducers/search.js | 32 +++++++++++ src/client/src/utils/search-table/hooks.js | 54 ++++++++++++++----- src/server/src/utils/elastic.js | 50 +++++++++-------- 10 files changed, 212 insertions(+), 58 deletions(-) diff --git a/src/client/src/components/Search/Search.js b/src/client/src/components/Search/Search.js index e952b9097..6656b0c44 100644 --- a/src/client/src/components/Search/Search.js +++ b/src/client/src/components/Search/Search.js @@ -35,6 +35,7 @@ const Search = ({ searchTerm, setSearchTerm, resetSearch, + resetSort, handlePageChange, addFilter, removeFilter, @@ -94,7 +95,10 @@ const Search = ({ setSearchTerm={setSearchTerm} resetSearch={resetSearch} isLoading={isLoading} - onEnterPress={() => sendRequest(searchTerm, options)} + onEnterPress={() => { + resetSort(); + sendRequest(searchTerm, options); + }} />
@@ -105,6 +109,7 @@ const Search = ({ })} onClick={(e) => { e.preventDefault(); + resetSort(); sendRequest(searchTerm, options); }} > @@ -257,6 +262,7 @@ Search.propTypes = { removeFilter: PropTypes.func.isRequired, removeFilters: PropTypes.func.isRequired, resetSearch: PropTypes.func.isRequired, + resetSort: PropTypes.func.isRequired, results: PropTypes.array, searchTerm: PropTypes.string.isRequired, sendRequest: PropTypes.func.isRequired, diff --git a/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js b/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js index 1d632d182..8080527c9 100644 --- a/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js +++ b/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js @@ -1,5 +1,11 @@ import "./awesomeTable.scss"; +import { + faSort, + faSortDown, + faSortUp, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import classNames from "classnames"; import PropTypes from "prop-types"; import React from "react"; @@ -9,6 +15,12 @@ import LeftArrow from "../shared/Icons/LeftArrow.jsx"; import RightArrow from "../shared/Icons/RightArrow.jsx"; import LoadSpinner from "../shared/LoadSpinner"; +const getSortIcon = (field, sortField, sortDirection) => { + if (field === sortField) { + return sortDirection === "asc" ? faSortDown : faSortUp; + } + return faSort; +}; const SearchAwesomeTable = ({ showPagination = false, pagination, @@ -17,6 +29,10 @@ const SearchAwesomeTable = ({ isLoading = false, data, fields, + // isSortable = true, + sortColumn, + sortField, + sortDirection, }) => { const handleRowClick = (event, element) => { if (event.target.tagName === "A") { @@ -29,9 +45,23 @@ const SearchAwesomeTable = ({ {fields.map((field) => { + const isSortableField = field.sortKey; return ( - + isSortableField && sortColumn(field.sortKey)} + > {field.headName} + {isSortableField && ( + + )} ); })} diff --git a/src/client/src/components/SearchResults/SearchResults.js b/src/client/src/components/SearchResults/SearchResults.js index 93365e388..cab6825e5 100644 --- a/src/client/src/components/SearchResults/SearchResults.js +++ b/src/client/src/components/SearchResults/SearchResults.js @@ -118,9 +118,7 @@ const SearchResults = ({ }); }, headName: "SIRET", - importantHead: true, link: ({ siret }) => `/establishment/${siret}`, - sortKey: "siret", }, { accessor: ({ etatAdministratifEtablissement, siret }) => { @@ -130,7 +128,7 @@ const SearchResults = ({ }); }, headName: "État", - sortKey: "etatadministratifetablissement", + sortKey: "etatAdministratifEtablissement", }, { accessor: (etablissement) => { @@ -140,7 +138,6 @@ const SearchResults = ({ }, headName: "Raison sociale / Nom", html: true, - sortKey: "enterprise_name", }, { accessor: (fields) => { @@ -172,10 +169,8 @@ const SearchResults = ({ }, headName: "Dirigeants", - importantHead: true, link: ({ fields }) => `/enterprise/${fields?.siren}#mandataires`, - sortKey: "dirigeants", }, { accessor: ({ etablissementSiege }) => { @@ -186,7 +181,6 @@ const SearchResults = ({ }); }, headName: "Catégorie établissement", - sortKey: "etablissementsiege", }, { accessor: ({ @@ -201,8 +195,8 @@ const SearchResults = ({ }); }, headName: "Code postal", - sortKey: "codepostaletablissement", }, + { accessor: ({ trancheEffectifsEtablissement, @@ -222,7 +216,7 @@ const SearchResults = ({ }); }, headName: "Effectif (DSN)", - sortKey: "lastdsntrancheeffectifsetablissement", + sortKey: "trancheEffectifsEtablissement", }, { accessor: ({ @@ -239,7 +233,7 @@ const SearchResults = ({ ); }, headName: "Activité", - sortKey: "activiteprincipaleetablissement", + sortKey: "codeActivitePrincipale", }, ]} /> diff --git a/src/client/src/containers/Search/Search.js b/src/client/src/containers/Search/Search.js index 9d5f1438f..2b17de616 100644 --- a/src/client/src/containers/Search/Search.js +++ b/src/client/src/containers/Search/Search.js @@ -2,11 +2,13 @@ import { groupBy, omit } from "lodash"; import { prop } from "lodash/fp"; import moment from "moment"; import React, { useEffect } from "react"; +import { useSelector } from "react-redux"; import SearchView from "../../components/Search"; import Http from "../../services/Http"; import { useResetSearch, + useResetSort, useSearchFilters, useSearchPage, useSearchQuery, @@ -23,7 +25,7 @@ const PAGE_SIZE = 10; const XLSX_DOC_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; -const formatLocationFilter = (filters) => { +const formatLocationFilter = (filters, sortDirection, sortField) => { const locationFilters = groupBy(filters.location, "type"); const codesCommunes = locationFilters?.commune?.map(prop("value")) || []; const departements = [ @@ -32,17 +34,26 @@ const formatLocationFilter = (filters) => { regionItem.regions.map((region) => region.value) ) || []), ]; - return { ...omit(filters, "location"), codesCommunes: normalizeCodeCommunes(codesCommunes), departements, + + sortField: sortField, + sortOrder: sortDirection, }; }; const Search = () => { const [searchQuery, setSearchQuery] = useSearchTerms(); const [searchPage, setSearchPage] = useSearchPage(); + const sortFieldFromStore = useSelector( + (state) => state?.search?.sort?.sortField + ); + + const sortOrderFromStore = useSelector( + (state) => state?.search?.sort?.sortOrder + ); const { filters, addFilter, removeFilter, removeFilters } = useSearchFilters(); @@ -50,13 +61,13 @@ const Search = () => { const { data, loading, error, makeQuery, query } = useSearchQuery(); const resetSearch = useResetSearch(); - + const resetSort = useResetSort(); useEffect(() => { if (searchQuery) { onSearch(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [sortField, sortDirection]); const downloadQuery = async () => { const trimmedQuery = searchQuery?.trim(); @@ -64,7 +75,11 @@ const Search = () => { const response = await Http.get("/downloadXlsx", { params: { q: trimmedQuery, - ...formatLocationFilter(filters), + ...formatLocationFilter( + filters, + sortOrderFromStore != null ? sortDirection : null, + sortFieldFromStore != null ? sortField : null + ), }, responseType: "blob", }); @@ -89,7 +104,11 @@ const Search = () => { setSearchPage(nextCurrentPage); makeQuery(searchQuery, { page: { current: nextCurrentPage - 1, size: PAGE_SIZE }, - params: formatLocationFilter(filters), + params: formatLocationFilter( + filters, + sortOrderFromStore != null ? sortDirection : null, + sortFieldFromStore != null ? sortField : null + ), }); }; @@ -97,7 +116,11 @@ const Search = () => { setSearchPage(1); makeQuery(searchQuery, { page: { current: 0, size: PAGE_SIZE }, - params: formatLocationFilter(filters), + params: formatLocationFilter( + filters, + sortOrderFromStore != null ? sortDirection : null, + sortFieldFromStore != null ? sortField : null + ), }); }; @@ -114,6 +137,7 @@ const Search = () => { searchTerm={searchQuery || ""} setSearchTerm={setSearchQuery} resetSearch={resetSearch} + resetSort={resetSort} handlePageChange={handlePageChange} addFilter={addFilter} removeFilter={removeFilter} diff --git a/src/client/src/services/Store/actions/search.js b/src/client/src/services/Store/actions/search.js index fc9f5e190..37c3b0739 100644 --- a/src/client/src/services/Store/actions/search.js +++ b/src/client/src/services/Store/actions/search.js @@ -6,6 +6,18 @@ export const setSearchTerm = (term) => (dispatch) => { type: types.SET_SEARCH_TERM, }); }; +export const setSearchSortOrder = (sortOrder) => (dispatch) => { + dispatch({ + sortOrder, + type: types.SET_SEARCH_SORT_ORDER, + }); +}; +export const setSearchSortField = (sortField) => (dispatch) => { + dispatch({ + sortField, + type: types.SET_SEARCH_SORT_FIELD, + }); +}; export const setSearchPage = (page) => (dispatch) => { dispatch({ @@ -33,3 +45,9 @@ export const resetSearch = () => (dispatch) => { type: types.RESET_SEARCH, }); }; + +export const resetSort = () => (dispatch) => { + dispatch({ + type: types.RESET_SORT, + }); +}; diff --git a/src/client/src/services/Store/constants/ActionTypes.js b/src/client/src/services/Store/constants/ActionTypes.js index 2a8fd2fd4..1709f2c8b 100644 --- a/src/client/src/services/Store/constants/ActionTypes.js +++ b/src/client/src/services/Store/constants/ActionTypes.js @@ -1,6 +1,9 @@ export const SET_SEARCH_TERM = "SET_SEARCH_TERM"; +export const SET_SEARCH_SORT_ORDER = "SET_SEARCH_SORT_ORDER"; +export const SET_SEARCH_SORT_FIELD = "SET_SEARCH_SORT_FIELD"; export const SET_SEARCH_PAGE = "SET_SEARCH_PAGE"; export const SET_SEARCH_FILTERS = "SET_SEARCH_FILTERS"; export const SET_SEARCH_SORT = "SET_SEARCH_SORT"; export const SET_SEARCH_RESULTS = "SET_SEARCH_RESULTS"; export const RESET_SEARCH = "RESET_SEARCH"; +export const RESET_SORT = "RESET_SORT"; diff --git a/src/client/src/services/Store/hooks/search.js b/src/client/src/services/Store/hooks/search.js index 5464c8679..9a363f8a3 100644 --- a/src/client/src/services/Store/hooks/search.js +++ b/src/client/src/services/Store/hooks/search.js @@ -5,9 +5,12 @@ import { useDispatch, useSelector } from "react-redux"; import { useElasticQuery } from "../../Elastic/elastic"; import { resetSearch, + resetSort, setSearchFilters, setSearchPage as setSearchPageAction, setSearchResults, + setSearchSortField as setSearchSortFieldAction, + setSearchSortOrder as setSearchSortOrderAction, setSearchTerm as setSearchTermAction, } from "../actions"; @@ -23,6 +26,17 @@ export const useSearchTerms = () => { return [searchTerm, setSearchTerm]; }; +export const useSearchSortTerms = () => { + const dispatch = useDispatch(); + const setSearchSortOrder = (term) => { + dispatch(setSearchSortOrderAction(term)); + }; + const setSearchSortField = (term) => { + dispatch(setSearchSortFieldAction(term)); + }; + + return [setSearchSortOrder, setSearchSortField]; +}; export const useSearchPage = () => { const dispatch = useDispatch(); @@ -39,12 +53,12 @@ export const useSearchFilters = () => { const dispatch = useDispatch(); const savedFilters = useSelector((state) => getSearchState(state).filters); // cache busting mechanism + const validFilters = savedFilters.etats ? savedFilters : { etats: ["A"], }; - const addFilter = (key, value) => { dispatch(setSearchFilters({ ...savedFilters, [key]: value })); }; @@ -104,3 +118,8 @@ export const useResetSearch = () => { return () => dispatch(resetSearch()); }; +export const useResetSort = () => { + const dispatch = useDispatch(); + + return () => dispatch(resetSort()); +}; diff --git a/src/client/src/services/Store/reducers/search.js b/src/client/src/services/Store/reducers/search.js index b93a13852..10d97204b 100644 --- a/src/client/src/services/Store/reducers/search.js +++ b/src/client/src/services/Store/reducers/search.js @@ -1,15 +1,20 @@ import { RESET_SEARCH, + RESET_SORT, SET_SEARCH_FILTERS, SET_SEARCH_PAGE, SET_SEARCH_RESULTS, SET_SEARCH_SORT, + SET_SEARCH_SORT_FIELD, + SET_SEARCH_SORT_ORDER, SET_SEARCH_TERM, } from "../constants/ActionTypes"; const initialState = { filters: { etats: ["A"], + sortField: null, + sortOrder: null, }, page: 1, results: { @@ -19,6 +24,8 @@ const initialState = { sort: { ascDirection: false, field: null, + sortField: null, + sortOrder: null, }, term: null, }; @@ -30,6 +37,31 @@ const search = (state = initialState, action) => { ...state, term: action.term, }; + case SET_SEARCH_SORT_ORDER: + return { + ...state, + sort: { + ...state.sort, + sortOrder: action.sortOrder, + }, + }; + case SET_SEARCH_SORT_FIELD: + return { + ...state, + sort: { + ...state.sort, + sortField: action.sortField, + }, + }; + case RESET_SORT: + return { + ...state, + sort: { + ...state.sort, + sortField: null, + sortOrder: null, + }, + }; case SET_SEARCH_PAGE: return { diff --git a/src/client/src/utils/search-table/hooks.js b/src/client/src/utils/search-table/hooks.js index 668b5e883..c8c202365 100644 --- a/src/client/src/utils/search-table/hooks.js +++ b/src/client/src/utils/search-table/hooks.js @@ -1,5 +1,8 @@ import { omit } from "lodash"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; + +import { useSearchSortTerms } from "../../services/Store/hooks/search"; export const useFilters = (defaultValue) => { const [filters, setFilters] = useState(defaultValue); @@ -20,22 +23,49 @@ export const useFilters = (defaultValue) => { }; export const useSort = () => { - const [sortDirection, setSortDirection] = useState("desc"); - const [sortField, setSortField] = useState(null); + const [setSearchSortOrder, setSearchSortField] = useSearchSortTerms(); + const sortFieldFromStore = useSelector( + (state) => state?.search?.sort?.sortField + ); + const sortOrderFromStore = useSelector( + (state) => state?.search?.sort?.sortOrder + ); + const [sortDirection, setSortDirection] = useState(sortOrderFromStore); + const [sortField, setSortField] = useState(sortFieldFromStore); + const { addFilter } = useFilters(); + + useEffect(() => { + if (sortFieldFromStore == null && sortOrderFromStore == null) { + setSortField(sortFieldFromStore); + setSortDirection(sortOrderFromStore); + } + }, [sortFieldFromStore, sortOrderFromStore]); const toggleSortField = (field) => { if (field !== sortField) { - setSortDirection("desc"); + // Set a new sort field and default direction to "desc" setSortField(field); - return; - } - - if (sortDirection === "desc") { - setSortDirection("asc"); - return; + setSearchSortField(field); + setSortDirection("desc"); + setSearchSortOrder("desc"); + addFilter("sortField", field); + addFilter("sortDirection", "desc"); + } else { + // Toggling sort direction if field is the same + if (sortDirection === "desc") { + setSortDirection("asc"); + setSearchSortOrder("asc"); + addFilter("sortDirection", "asc"); + } else if (sortDirection === "asc") { + // Reset on the third click + setSortField(null); + setSortDirection(null); + setSearchSortField(null); + setSearchSortOrder(null); + addFilter("sortField", null); + addFilter("sortDirection", null); + } } - - setSortField(null); }; return { diff --git a/src/server/src/utils/elastic.js b/src/server/src/utils/elastic.js index 15a00b15c..07cac3e06 100644 --- a/src/server/src/utils/elastic.js +++ b/src/server/src/utils/elastic.js @@ -1,5 +1,4 @@ import config from "config"; - import { Client } from "@elastic/elasticsearch"; import codesNaf from "@socialgouv/codes-naf"; @@ -17,7 +16,7 @@ const filtersFieldMap = { departement: "departement", codesPostaux: "codesPostalEtablissement", tranchesEffectifs: "trancheEffectifsEtablissement", - naf:"codeActivitePrincipale" + naf: "codeActivitePrincipale", }; const getCodeNafLibelle = (code) => @@ -45,27 +44,14 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { query: { bool: { must: [ - { - match: { - "dirigeants.nom": { - query: dirigeant.nom, - }, - }, - }, - { - match: { - "dirigeants.prenom": { - query: dirigeant.prenom, - }, - }, - }, + { match: { "dirigeants.nom": dirigeant.nom } }, + { match: { "dirigeants.prenom": dirigeant.prenom } }, ], }, }, }, }); - } - if (dirigeant.nom && !dirigeant.prenom) { + } else if (dirigeant.nom) { dirigeantConditions.push({ nested: { path: "dirigeants", @@ -74,8 +60,7 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { }, }, }); - } - if (dirigeant.prenom && !dirigeant.nom) { + } else if (dirigeant.prenom) { dirigeantConditions.push({ nested: { path: "dirigeants", @@ -86,6 +71,7 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { }); } } + return { ...(query ? { min_score: 20 } : {}), query: { @@ -110,7 +96,6 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { ] : [] ), - ...(query ? [ { @@ -129,7 +114,6 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { boost: 100, }, }, - { multi_match: { query, @@ -141,7 +125,6 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { minimum_should_match: "100%", }, }, - ...(siretOrSirenQuery ? [ { @@ -213,7 +196,7 @@ const formatElasticResult = (hit) => { export const getElasticQueryParams = (req) => { const query = (req.query["q"] || "").trim(); const activites = req.query["activites"] || []; - const naf=req.query["naf"] || []; + const naf = req.query["naf"] || []; const codesCommunes = req.query["codesCommunes"] || []; const codesPostaux = req.query["codesPostaux"] || []; const departement = req.query["departements"] || []; @@ -221,6 +204,8 @@ export const getElasticQueryParams = (req) => { const dirigeant = req.query["dirigeant"] ? JSON.parse(req.query["dirigeant"]) : null; + const sortField = req.query["sortField"]; + const sortOrder = req.query["sortOrder"]; let etats = req.query["etats"] || []; const siege = (req.query["siege"] || "").trim(); @@ -239,13 +224,26 @@ export const getElasticQueryParams = (req) => { codesPostaux, tranchesEffectifs, dirigeant, - naf + naf, + sortField, + sortOrder, }; }; export const requestElastic = async (params, { from, size }) => { - console.log(params); const body = makeQuery(params); + + // Ajouter le tri après avoir construit la requête de filtrage + if (params.sortField && params.sortOrder) { + body.sort = [ + { + [params.sortField]: { + order: params.sortOrder, + }, + }, + ]; + } + const { body: { hits: { total, hits }, From 2fdb5e329011ebeae8c20e549a840ca72df2f786 Mon Sep 17 00:00:00 2001 From: Social Groovy Bot <45039513+SocialGroovyBot@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:19:50 +0000 Subject: [PATCH 2/3] chore(release): version 27.106.0 # [27.106.0](https://github.com/SocialGouv/fce/compare/v27.105.0...v27.106.0) (2024-09-25) ### Features * Sort search result ([#351](https://github.com/SocialGouv/fce/issues/351)) ([f4d2f0b](https://github.com/SocialGouv/fce/commit/f4d2f0b782075efe6d71a70f63b2416ab3e98b32)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b68727b3..a4c837570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [27.106.0](https://github.com/SocialGouv/fce/compare/v27.105.0...v27.106.0) (2024-09-25) + + +### Features + +* Sort search result ([#351](https://github.com/SocialGouv/fce/issues/351)) ([f4d2f0b](https://github.com/SocialGouv/fce/commit/f4d2f0b782075efe6d71a70f63b2416ab3e98b32)) + # [27.105.0](https://github.com/SocialGouv/fce/compare/v27.104.0...v27.105.0) (2024-09-19) diff --git a/package.json b/package.json index 6f8349b92..c118e075e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fce", - "version": "27.105.0", + "version": "27.106.0", "description": "", "author": "commit42", "private": true, From e01cb0989223962da3a756aaed67071a5c28a7ae Mon Sep 17 00:00:00 2001 From: Imen Chermiti <127976664+ImenOuidou@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:14:03 +0200 Subject: [PATCH 3/3] Update dev env (#355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add pgadmin * chore: fix key * chore: missing keys * chore: typo * chore: numeric user * chore: secret * chore: containerPort * chore: auth * chore: probespath * chore: update dev env with bce integration data --------- Co-authored-by: Gary van Woerkens Co-authored-by: Matéo Mévollon --- .kontinuous/env/dev/templates/server.sealed-secret.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.kontinuous/env/dev/templates/server.sealed-secret.yaml b/.kontinuous/env/dev/templates/server.sealed-secret.yaml index 0978effb5..0e6ca803c 100644 --- a/.kontinuous/env/dev/templates/server.sealed-secret.yaml +++ b/.kontinuous/env/dev/templates/server.sealed-secret.yaml @@ -40,8 +40,8 @@ spec: API_TOKEN: AgA5TseHibqsLxr1Cvom/JCKKSQNwFl8uWNS3sUf40jMc/bcyYWUyhQqYfsb/BCzzXpPP6O/S6Egy858/XZ2VshfIJ31iYYpoRydEzWZnOln4f9woxLc3U6pG5WP94QRJCpKUrsfTU68pZ+UDU3xnswXvQNwGgjb/F9ZTimwS585qHqdyuuFT6Fexf7kxPQXys1J7eAHUuqXcaUOrl/WDdzSuFjdA1HeKj6f747kNbT3wreEHqFtQTK+lNaSZUVZ5oMEJ5O3aGJd+eEWEHf9dvkKdVO1giGhH/hB42ziGq4bCVGFC4NerMUE/X7HYmtleR0MOiycTlBFzTbYkGugrMhgUEL4KxM/FXyCPfNr4YUbHJLOykUTeWstcclsx9Qhn3uRhdE9yXjabnh1NhLgjxp5ttD1EQGYQ8+floKGCsuXi/AMHvp0ewi1EJMV7WP5pDjx+3ywzDdfSnYolOI7iXkshEuZSkgIjBeU4K/wiqrcWbC3KX4Evgr+SoXO5DAucAu6InSjGdbRqxKfuhJcx1l12ggMOivYHt71CWJ3mFKS/vHLuTXwUOhpmr6Ifl6rxnMRHObmBjZdJmjF7WWWqkkpSxFJ9qWiNoKJYw/DtgNJlRyUQazjt6Qda8jv7XfDHuuO5qdChIjZU2ir2wwkLEd3GtlJRuJYSMq/SF8m0d+rShnWexM/MUe7RZIBdNTWwnflm0AhWvlX7JXe/5+GyraHa/MCM+QdQZvw/KfSAOPVNA== #BCE - BCE_URL: AgAD7FqkOzeNjQlBIuEx4Yp3ca+fUPTZ86Pcgg8+bSoiCqtD/6XIzXFPlIU86/rZGmzvG0xhObdDD4E9baACkf45R7v6iG6vnrsb5heV/D3E92T0896LkL9p2qflv/mf69gr5bgmgUNjP2ZfF3HaEMYMhzXA+EKjGyMZB2agy2B24Zvstx3EnkMI54U/VN5NFPmq13G9NCaZoAF9G+cyPSGeSTg65iVBAw4SZXIcTWHl9u5aQXhfT7VCfcb5U7r4ArD19FrKm8WduaGKAzFFxxPgM6Kr3XJnV7oyUSso1Fbm4VNw9sVpy8D9wZjIaNY/AiLjW8XQyRmm2tPA6dLBQAekQambVI5Ve/Kjujn9Bc4p9NoN3MMvvAedoPqakmr5gwWsiPHk4kMexDAQGa8Ww4JOVecYS2Qrw9W8tU/MeSfoj2qbixcBeqqToA7/2FjK9INPWumG7USxzWvGP+uYj0OxOABpMuzcLnruha0TDDCEmwSBE8Vzh6sIznxC2DoOLVfijsS5jGNSARORnmNxsUlThDlXoBs/FVR8nk9Wa8DHBne9ey8ZTuGBIy8OsR70HNv1NS14HEezzObMTzh+rD11XdOjZqwp8e9/rA3qvUNkLzuwgqPh3wEyTPwEQJ3TXwiB3BgyLEKgTUfhnOLyOwyrCyHLxclCWuuKVq7whjScaTMZ+o7YaSbrkP9+AB+S109KqHb1eKQfWwqOYCGWpvfPlRL9yZ7vVkY1i10wYXx6yyOkEx4/fn8i4FY= - BCE_TOKEN: AgCHieYGkw5r0WFtSg6OOUe5E7aMGtG2L0Z5hcLCTbvaKVg40R2nfHGQ198Q5zvzwYnswtLQeTSTrlcBivrRGR/8aPhs5JKvO3AXGlqThHeLB9s2DUv9yJ0+oQD66gK3xr15sN3oR+wqCudq2aNdFo67Jyz5Q0hlJRmGdzyHk6Rr0b80CcA+vHE0v0HDLiYrioju+b9QXeJwlxkRXv9nOKhaGJsIqMKwR67vYn8zGz63QuiVCB/zuD8xJkbpgI1omKZYyVnQSgGyKUzYcvly+9khnwSkpy8Kh5/lcjgEhFpWWRCeP1o21IJYpMg4QcFWKvg7Depf11YU1wSnEsA985211IO21QMUTIopnpgpIA31yj3RfzFTuyrrMYp8rqOc6qViHfWKAwqqEHvguBXVTKtsvFCpvX/y9J7bj4qo1ayq0qZqg9yctGHqT2xCnvuQPXSVntBY/I8iUDRV3zKoUYJdwpDsLxVoCG9ItGPN64tKCxoIE5rFGxCugcfVf0cmfL+caIEqfk5GS59x9Ybw3X73aBKTbz3RPIBNeNQ9hnuagFx7qdOikHvpa/aEmFUDoxXyHWzgR4btkwOWIiF0MPhf45KGS8C52NJpPXHYFAKv/lDG4lV2GZSq28HTSUzMX2He0tcQvE/ddpdWKitN0DUiy4C8RbgjnmD6IeU0owVsTuWlbNp215Q0j0zdYv58TJjTwYlRfP+SosuQ9EpuDj++jgUh3idYPiXzWp4dv/7xb02bDfgG2xj7x2rlokeC1PosCP5wX6axlPH/d0ts/crwU2Jzlxoiu9fuDlZ9mf3bTJd8ZskTZid0PdMJCFyWY8M7aaSKkK5s6NhhPHPvhD60+1wXx4buyavbWNdRnjcLg7gUiI2wwDeU5kVtlQ72pE8Oje1jEPeu58fUrUQpv+Q3/M9goOte7sBkBrjS4S6SeHzYX8n0U1uCDMV/vC58J2PxZvboj8u8Ng4emRKCItkflDp87n6EV9YbolTtPp6E3YPJwhXPdjsaq7EfrDkbq0XLNXC+D2/JNNSfA9vFoihOUGpLxK/zL5gerXE3se84Nqm89lEvAMUlqISJq5MD2Msjy7QbDgsOqJYO3Sqk/fDKPRAr/dllVpHWV48Hc7SzOqxP7wyKKjynzQHtYRy6HIgnUgjQVhDHx2CoskNIS0ygQIB5YjrlGZukzikP1t79Xr/BypAPVqkuljRpE4VlOu2SIK8bEBWJWl8hux8I8EKrt7OYTGgLko059qkEZjjuEMO9XuRBh1P5ZxauV6hi9OP8JU9f0aY7cCGvhbbxQWy+NS9HATxjUoerIaQ9jaq3/viBUoavqs0xVwIxOQ0LXf/yUnRMU2SSp78sB9oJ57aQ2mmF4Dn+mSXgB2RA+oWZn1NsnayYjbw8diVnnXwRyN/Vc2+CN0ag0TCoA912XcjNctjhdFUwk1DonrTA+7elhDSBjNFBnlJaczIX2uwz8ShhAXwVMLSC9uDBjW3fty+gnwdCTHWJu6xOqPYBKbCnqyhw0vmCfSR+LpNMWgTDoD5sE6IYye2+cZwL4za9tzpAw1wi9Oqkp5o0kzkqnX+rjG1vLsVai47n0yLO7GBN25kOMcIYoBUgsh2U7APYXxZl8cMsaTFz+hSqKZ+0Rd+1yRuV5OgKZ370Q3RxwzANLOogPWutircbK/aE039bIsUqCQr9qY2zXSf1hhuZ8iQhz71BXvHqTO7rib9OK8wfRSVTidE1YUABEJNpwjlK2U/WdBvoP2WbKhHmzxa3Ej+b0K88kwJ1Fn8oWZyu9xfSh4SQprhNWcK+K9J7i6+LVDzU+EhoS+Gz7pbCtTbXsQGse9FTwCdtif2O1mpc89ZlF4kQ1FaEvpLPNHdbjoCdAIPLM8ZAFB/TVCEOLlUyW4y/1m2G2l3pZr+UiXWZLVD8QA== + BCE_URL: AgCqlK4k2JFDjSa37o2zoon5aCmkFlRTNH/oA5enfna6IZGoXMJOH0EOTI9Yc63zeMrPPqHcnbMw3h5ZnDiQCxgWD+z4vKhjh9XfsDQL3KDdXmvFhnyMWSl+8rvr56b1fH1gSEoC0e/sgpwE+fqv723AcPOMZRrl8ama1gZ+T92gcChQ6tIFSkKc6x9O2o94yfAbACGH+kLV5rWegnde2iK6adlVUraHtHpOvsOnzr4RrUd2alJWQHTUCan1L6IQX1eA2HfYUUhKC4tJ6COQPgBgNTCFHYky+BrVGscZjmGumr4A4Av9FbpCQHc5iBKEac2Zl2vooYMU1dbUFE01SAe1tBSnts2aRAHEH/fB34vUzIVWAgya8jUlHaabwfe+e1c5kxVcNyZLPNfEgRUxp1HYCSgU4nbkDOAP+OV4Cue99Ye93X5YQRj0Bi6ASpxXT2nE1lP1EKLewLB8at4mPvkRKBI2vfIXfA6SXKal2wfo1vdGzz7VcmeGZKYetBsfJFrEhpgVsuQOPh/R9nR+zaJSG0QbKoR9An889zV5IW26I3CRra47cZ3gNI08MIz2NXvKDuQFsBL0EAZ85JzNM8KjVRRJ40EwqdatSwxvyP09qTgwS8JXBE2nTm6G5t9L3h3bfOO6zdV8SwfHlRfjmzCwy7Rp4nLUlN36CrydyKh+WGYW1IHpDwImmZOhgOdbUP4amde6hePF3qbmR0xusTbBuBU+kziHDFxaBtEOf/bihbgjruMnz0N+ow== + BCE_TOKEN: AgCps9Mhf3nJXIMAlJFMz5uc9nrDLt300W9DhppI0s9Y+7psqYSGFJ+jUiAzBhxgrE4fHxHOFEz1yuBtnAol0FIK3hZpGSXVMC84vTL/6H80YEAEHEez3vVvTGiejGWmyjxeYnq8OTybstXJl2VOOk1zQIaKCzYpcvjaW0DHDgCuxSmI+JOT2h89YPgv5IhSphu0u9f9Xzv13Kv8Ds7sKyLpkQCoylWtlF3MDSg656leBoLVk8Vj3olh18pKjujRrqZYgdyA36KFxy+Z8jbGUuld7I6bqPTCdX9vo79s5stRn39Hrw5o/D+ZjVFYBDC7KnqGlz+KGJTI0BFHw1I1qC1qgN3MpvINlKb6QhXWg9osr1Fe5JmGc1cu8Ltx67x2g5ScgCfM5HFpJAIw2Cph/lGs0WsuXp44Cktyel8wxHbi5nzBUYPnjVqNc8iqxaHPKPucuiVnonojAg/U7Xuq5v3z5+bj2JalHXCISB2xWH0QfLJfzLl0nj7sbBf0q1L+W0hKQkraHvDJ8PtnkX2LkpyXSOChnS8lN1++ZH2b8sS2bEUP6konaqgLyuOAAPNOJtwVX/2Al5L981XuUyUeHkG4gGapolPc4rtHVpa6AD6DqhbM8JLJWof1tBCnWfRYujW/zglXtd4vz7eyLz4OxEXOYe5CRZ0nfTLKtVK52WJX+TJ8FhRsnBWu4Z72Pw70U7zJILiWnP1HzOm8CznDP6/XAJJDdLDIn9hOpHzn7xqVQvRlta/UyuoRy+YdRUksaLXxJrYI35ZGG4niTpS/YKQtdqiHOAcWZSp52FhtliiT1E5HK1AINLtLTCbpLPoqnZiFx+ONYf+duzVX01x/C9kimBFvDIEweKsAh+a0ht3LBws6ynhkhCmSavLqT+nlP8zs6vc455RKNFfKhC8uCn8WIsIWG2s/MpteBpJpA1bqdyqHGrgnh9jY18NrDelNtXqILpLFs3usn+IdqzcNrSD9yoZEYiQhuyBmPAaNsqE5xTwaQMjg8Vzu8IRG4dD57oudMdpGErkvncX/dvyt0yAAG3Jbfj3frdXpo7Vj+1jpuvOhSsZPhBwgjjld+ccY98Xm5vljLnHFiQioIoWbjNZw/S1uo4Q6mrDxNAZLXbTwjPE7BgLknHT0lMrMva8Pr+Ej9yn/LENmVCiHiwmmkWucaiXu4/Phom8rA+3P3kMiZfETHnZmMb4Zumqk1nSc89fcEd7S5tGmGYlY+ofeP3d5pdUQ+YJEWJtyvnKL5QfCpfkgNOERx5bqL0xOLKaZ387vD0DKUDfkdvAbvVTbRLJmLc/Yc9lEFNrmHn0Ej1B5+QhheMvsjFFiu2VacOX+2Wl7qTOL1WWvR2qCn5GHuO+nroykfl0OVaEsuhWJK/mSzpIcHLF5NbNk+IIJe+T3Q3oSMGThUmLhUX70axJF/qMxg1vB/7gNMwK03bbqldkVVzy3jZxb3bPpKND3y4zBx4V61qdE+gQxuEAwFBss87AH2Mohh/0KI7OINhibA8s9V65Xn96uFIgpks0YXFIvDOG6rPdqYyEiNznoJB51SxajLYZQO//ko3IBexJogCIxq4zdIAcAJMdhKbDYbgWGH2R6LeC6BhcDupr0g5UwZOUZaVrjbk/6nuE7rQS3i3uXOVMjg0uL5dCfWb1AfHSZnGNzeCOq1XCpQKu+0zXJaNq/b3Ohyiovf1Gt/qpofidULi0RgVHaRbcz3iN+xqVP1BpLLnC10Uwg4B9wrby2r5BQzb+xe9s0a0vHRJzIf8LkdBUjCdaY2QElzm7E8wb9dOqHvdMSmAvY+jT9WjY+Vh6/whKPrnztjdDaIbJ4LT8SyZYxjDA550uitOVXdVySy3vxzVmOPREAwerCCKq/ template: metadata: annotations: *a1