From e0cbee59db2ba079a47db66dacc41b1671a44ffb Mon Sep 17 00:00:00 2001 From: Suren Date: Mon, 16 Dec 2024 13:06:10 +0530 Subject: [PATCH] #10236: Fix - Legend filter persisting issue when editing style --- web/client/api/WMS.js | 20 ++- .../components/StyleBasedWMSJsonLegend.jsx | 45 +++--- .../StyleBasedWMSJsonLegend-test.jsx | 131 ++++++++++++++---- web/client/translations/data.de-DE.json | 3 +- web/client/translations/data.en-US.json | 3 +- web/client/translations/data.es-ES.json | 3 +- web/client/translations/data.fr-FR.json | 3 +- web/client/translations/data.it-IT.json | 4 +- web/client/utils/LegendUtils.js | 2 +- 9 files changed, 155 insertions(+), 59 deletions(-) diff --git a/web/client/api/WMS.js b/web/client/api/WMS.js index 6f680ff988..94f3e05aac 100644 --- a/web/client/api/WMS.js +++ b/web/client/api/WMS.js @@ -10,7 +10,7 @@ import urlUtil from 'url'; import { isArray, castArray, get } from 'lodash'; import xml2js from 'xml2js'; import axios from '../libs/ajax'; -import { getConfigProp } from '../utils/ConfigUtils'; +import ConfigUtils, { getConfigProp } from '../utils/ConfigUtils'; import { getWMSBoundingBox } from '../utils/CoordinatesUtils'; import { isValidGetMapFormat, isValidGetFeatureInfoFormat } from '../utils/WMSUtils'; const capabilitiesCache = {}; @@ -323,15 +323,25 @@ export const getSupportedFormat = (url, includeGFIFormats = false) => { let layerLegendJsonData = {}; export const getJsonWMSLegend = (url) => { - const request = layerLegendJsonData[url] - ? () => Promise.resolve(layerLegendJsonData[url]) - : () => axios.get(url).then((response) => { + let request; + + // enables caching of the JSON legend for a specified duration, + // while providing the possibility of re-fetching the legend data in case of external modifications + const cached = layerLegendJsonData[url]; + if (cached && new Date().getTime() < cached.timestamp + (ConfigUtils.getConfigProp('cacheExpire') || 60) * 1000) { + request = () => Promise.resolve(cached.data); + } else { + request = () => axios.get(url).then((response) => { if (typeof response?.data === 'string' && response.data.includes("Exception")) { throw new Error("Faild to get json legend"); } - layerLegendJsonData[url] = response?.data?.Legend; + layerLegendJsonData[url] = { + timestamp: new Date().getTime(), + data: response?.data?.Legend + }; return response?.data?.Legend || []; }); + } return request().then((data) => data).catch(err => { throw err; }); diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index 774d3dedf4..2e5e62c338 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -71,11 +71,11 @@ class StyleBasedWMSJsonLegend extends React.Component { const prevLayerStyle = prevProps?.layer?.style; const currentLayerStyle = this.props?.layer?.style; - const prevFilter = getLayerFilterByLegendFormat(prevProps?.layer, LEGEND_FORMAT.JSON); - const currFilter = getLayerFilterByLegendFormat(this.props?.layer, LEGEND_FORMAT.JSON); + const [prevFilter, currFilter] = [prevProps?.layer, this.props?.layer] + .map(_layer => getLayerFilterByLegendFormat(_layer, LEGEND_FORMAT.JSON)); // get the new json legend and rerender in case of change in style or layer filter - if (currentLayerStyle !== prevLayerStyle + if (!isEqual(prevLayerStyle, currentLayerStyle) || !isEqual(prevFilter, currFilter) || !isEqual(prevProps.mapBbox, this.props.mapBbox) ) { @@ -87,6 +87,7 @@ class StyleBasedWMSJsonLegend extends React.Component { const newLayerFilter = updateLayerLegendFilter(this.props?.layer?.layerFilter); this.props.onChange({ layerFilter: newLayerFilter }); } + getLegendData() { let jsonLegendUrl = this.getUrl(this.props); if (!jsonLegendUrl) { @@ -109,6 +110,7 @@ class StyleBasedWMSJsonLegend extends React.Component { } return null; }; + getUrl = (props, urlIdx) => { if (props.layer && props.layer.type === "wms" && props.layer.url) { const layer = props.layer; @@ -147,9 +149,10 @@ class StyleBasedWMSJsonLegend extends React.Component { } return ''; } + renderRules = (rules) => { - const isLegendFilterIncluded = get(this.props, 'layer.layerFilter.filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); - const legendFilters = isLegendFilterIncluded ? isLegendFilterIncluded?.filters : []; + const interactiveLegendFilters = get(this.props, 'layer.layerFilter.filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); + const legendFilters = get(interactiveLegendFilters, 'filters', []); const isPreviousFilterValid = this.checkPreviousFiltersAreValid(rules, legendFilters); return ( <> @@ -163,22 +166,26 @@ class StyleBasedWMSJsonLegend extends React.Component { : null} - {(rules || []).map((rule) => { - const activeFilter = legendFilters?.some(f => f.id === rule.filter); - const isFilterDisabled = this.props?.layer?.layerFilter?.disabled; - return ( -
this.filterWMSLayerHandler(rule.filter)}> - - {rule.name || rule.title || ''} -
- ); - })} + {isEmpty(rules) + ? + : rules.map((rule, idx) => { + const activeFilter = legendFilters?.some(f => f.id === rule.filter); + const isFilterDisabled = this.props?.layer?.layerFilter?.disabled; + return ( +
this.filterWMSLayerHandler(rule.filter)}> + + {rule.name || rule.title || ''} +
+ ); + }) + } ); }; + render() { if (!this.state.error && this.props.layer && this.props.layer.type === "wms" && this.props.layer.url) { return <> @@ -202,12 +209,14 @@ class StyleBasedWMSJsonLegend extends React.Component { ); } + filterWMSLayerHandler = (filter) => { const isFilterDisabled = this.props?.layer?.layerFilter?.disabled; if (!filter || isFilterDisabled) return; const newLayerFilter = updateLayerLegendFilter(this.props?.layer?.layerFilter, filter); this.props.onChange({ layerFilter: newLayerFilter }); }; + checkPreviousFiltersAreValid = (rules, prevLegendFilters) => { const rulesFilters = rules.map(rule => rule.filter); return prevLegendFilters?.every(f => rulesFilters.includes(f.id)); diff --git a/web/client/plugins/TOC/components/__tests__/StyleBasedWMSJsonLegend-test.jsx b/web/client/plugins/TOC/components/__tests__/StyleBasedWMSJsonLegend-test.jsx index c648c63694..1c3b91ce8e 100644 --- a/web/client/plugins/TOC/components/__tests__/StyleBasedWMSJsonLegend-test.jsx +++ b/web/client/plugins/TOC/components/__tests__/StyleBasedWMSJsonLegend-test.jsx @@ -14,8 +14,39 @@ import axios from '../../../../libs/ajax'; import StyleBasedWMSJsonLegend from '../StyleBasedWMSJsonLegend'; import expect from 'expect'; import TestUtils from 'react-dom/test-utils'; +import { INTERACTIVE_LEGEND_ID } from '../../../../utils/LegendUtils'; let mockAxios; +const rules = [ + { + "name": ">= 159.05 and < 5062.5", + "filter": "[field >= '159.05' AND field < '5062.5']", + "symbolizers": [{"Polygon": { + "uom": "in/72", + "stroke": "#ffffff", + "stroke-width": "1.0", + "stroke-opacity": "0.35", + "stroke-linecap": "butt", + "stroke-linejoin": "miter", + "fill": "#8DD3C7", + "fill-opacity": "0.75" + }}] + }, + { + "name": ">= 5062.5 and < 20300.35", + "filter": "[field >= '5062.5' AND field < '20300.35']", + "symbolizers": [{"Polygon": { + "uom": "in/72", + "stroke": "#ffffff", + "stroke-width": "1.0", + "stroke-opacity": "0.35", + "stroke-linecap": "butt", + "stroke-linejoin": "miter", + "fill": "#ABD9C5", + "fill-opacity": "0.75" + }}] + } +]; describe('test StyleBasedWMSJsonLegend module component', () => { beforeEach((done) => { @@ -45,35 +76,7 @@ describe('test StyleBasedWMSJsonLegend module component', () => { "Legend": [{ "layerName": "layer00", "title": "Layer", - "rules": [ - { - "name": ">= 159.05 and < 5062.5", - "filter": "[field >= '159.05' AND field < '5062.5']", - "symbolizers": [{"Polygon": { - "uom": "in/72", - "stroke": "#ffffff", - "stroke-width": "1.0", - "stroke-opacity": "0.35", - "stroke-linecap": "butt", - "stroke-linejoin": "miter", - "fill": "#8DD3C7", - "fill-opacity": "0.75" - }}] - }, - { - "name": ">= 5062.5 and < 20300.35", - "filter": "[field >= '5062.5' AND field < '20300.35']", - "symbolizers": [{"Polygon": { - "uom": "in/72", - "stroke": "#ffffff", - "stroke-width": "1.0", - "stroke-opacity": "0.35", - "stroke-linecap": "butt", - "stroke-linejoin": "miter", - "fill": "#ABD9C5", - "fill-opacity": "0.75" - }}] - }] + rules }] }]; }); @@ -89,4 +92,74 @@ describe('test StyleBasedWMSJsonLegend module component', () => { expect(legendRuleElem).toBeTruthy(); expect(legendRuleElem.length).toEqual(2); }); + it('tests legend with empty rules', async() => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wms', + url: 'http://localhost:8080/geoserver1/wms' + }; + mockAxios.onGet(/geoserver1/).reply(() => { + return [200, { + "Legend": [{ + "layerName": "layer01", + "title": "Layer1", + "rules": [] + }] + }]; + }); + const comp = ReactDOM.render(, document.getElementById("container")); + await TestUtils.act(async() => comp); + + const domNode = ReactDOM.findDOMNode(comp); + expect(domNode).toBeTruthy(); + + const legendElem = document.querySelector('.wms-legend'); + expect(legendElem).toBeTruthy(); + expect(legendElem.innerText).toBe('layerProperties.interactiveLegend.noLegendData'); + const legendRuleElem = domNode.querySelectorAll('.wms-json-legend-rule'); + expect(legendRuleElem.length).toBe(0); + }); + it('tests legend with incompatible filter rules', async() => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wms', + url: 'http://localhost:8080/geoserver2/wms', + layerFilter: { + filters: [{ + id: INTERACTIVE_LEGEND_ID, + filters: [{ + id: 'filter1' + }] + }] + } + }; + mockAxios.onGet(/geoserver2/).reply(() => { + return [200, { + "Legend": [{ + "layerName": "layer01", + "title": "Layer1", + rules + }] + }]; + }); + const comp = ReactDOM.render(, document.getElementById("container")); + await TestUtils.act(async() => comp); + + const domNode = ReactDOM.findDOMNode(comp); + expect(domNode).toBeTruthy(); + + const legendElem = document.querySelector('.wms-legend'); + expect(legendElem).toBeTruthy(); + const legendRuleElem = domNode.querySelector('.wms-legend .alert-warning'); + expect(legendRuleElem).toBeTruthy(); + expect(legendRuleElem.innerText).toContain('layerProperties.interactiveLegend.incompatibleFilterWarning'); + const resetLegendFilter = domNode.querySelector('.wms-legend .alert-warning button'); + expect(resetLegendFilter).toBeTruthy(); + }); }); diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 1ce244e627..a10ad99a65 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -243,7 +243,8 @@ "disableFeaturesEditing": "Deaktivieren Sie die Bearbeitung der Attributtabelle", "interactiveLegend": { "incompatibleFilterWarning": "Angewendete Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel. Klicken Sie auf Zurücksetzen, um die Legendenfilter zu entfernen", - "resetLegendFilter": "Zurücksetzen" + "resetLegendFilter": "Zurücksetzen", + "noLegendData": "Keine Legenden Elemente zum Anzeigen" } }, "localizedInput": { diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 2baeefa050..86c5aa1f2a 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -243,7 +243,8 @@ "disableFeaturesEditing": "Disable editing on Attribute table", "interactiveLegend": { "incompatibleFilterWarning": "Applied legend filters are incompatible with the active layer filter. Click on reset to remove legend filters", - "resetLegendFilter": "Reset" + "resetLegendFilter": "Reset", + "noLegendData": "No legend items to show" } }, "localizedInput": { diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 6c37273a28..d7d4fb5825 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -243,7 +243,8 @@ "disableFeaturesEditing": "Deshabilitar la edición en la tabla de atributos", "interactiveLegend": { "incompatibleFilterWarning": "Los filtros de leyenda aplicados son incompatibles con el filtro de capa activo. Haga clic en restablecer para eliminar los filtros de leyenda", - "resetLegendFilter": "Restablecer" + "resetLegendFilter": "Restablecer", + "noLegendData": "No hay elementos de leyenda para mostrar" } }, "localizedInput": { diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 64947572f9..0dd0188023 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -243,7 +243,8 @@ "disableFeaturesEditing": "Désactiver la modification sur la table attributaire", "interactiveLegend": { "incompatibleFilterWarning": "Les filtres de légende appliqués sont incompatibles avec le filtre de couche actif. Cliquez sur réinitialiser pour supprimer les filtres de légende", - "resetLegendFilter": "Réinitialiser" + "resetLegendFilter": "Réinitialiser", + "noLegendData": "Aucun élément de légende à afficher" } }, "localizedInput": { diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 57709d4601..81ab902518 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -243,8 +243,8 @@ "disableFeaturesEditing": "Disabilita la modifica sulla tabella degli attributi", "interactiveLegend": { "incompatibleFilterWarning": "I filtri della legenda applicati sono incompatibili con il filtro del livello attivo. Clicca su reset per rimuovere i filtri della legenda", - "resetLegendFilter": "Reset" - } + "resetLegendFilter": "Reset", + "noLegendData": "Nessun elemento della legenda da mostrare" } }, "localizedInput": { "localize": "Traduci questo testo...", diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js index b7246f2e52..18405b938e 100644 --- a/web/client/utils/LegendUtils.js +++ b/web/client/utils/LegendUtils.js @@ -20,7 +20,7 @@ export const LEGEND_FORMAT = { JSON: "application/json" }; -export const getLayerFilterByLegendFormat = (layer, format) => { +export const getLayerFilterByLegendFormat = (layer, format = LEGEND_FORMAT.JSON) => { const layerFilter = layer?.layerFilter; if (layer && layer.type === "wms" && layer.url) { if (format === LEGEND_FORMAT.JSON && !isEmpty(layerFilter)) {