diff --git a/web/client/actions/__tests__/catalog-test.js b/web/client/actions/__tests__/catalog-test.js index b50ae764b2..17fdf54de5 100644 --- a/web/client/actions/__tests__/catalog-test.js +++ b/web/client/actions/__tests__/catalog-test.js @@ -238,6 +238,13 @@ describe('Test correctness of the catalog actions', () => { expect(retval).toExist(); expect(retval.type).toBe(ADD_SERVICE); }); + it('addService with options', () => { + const options = {"test": "1"}; + var retval = addService(options); + expect(retval).toExist(); + expect(retval.type).toBe(ADD_SERVICE); + expect(retval.options).toEqual(options); + }); it('addCatalogService', () => { var retval = addCatalogService(service); diff --git a/web/client/actions/catalog.js b/web/client/actions/catalog.js index d4fd402bc2..fd41eeab71 100644 --- a/web/client/actions/catalog.js +++ b/web/client/actions/catalog.js @@ -170,9 +170,10 @@ export function changeUrl(url) { url }; } -export function addService() { +export function addService(options) { return { - type: ADD_SERVICE + type: ADD_SERVICE, + options }; } export function addCatalogService(service) { diff --git a/web/client/api/catalog/COG.js b/web/client/api/catalog/COG.js index a566aa4813..a7de6e4814 100644 --- a/web/client/api/catalog/COG.js +++ b/web/client/api/catalog/COG.js @@ -8,8 +8,9 @@ import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; +import isNil from 'lodash/isNil'; import { Observable } from 'rxjs'; -import { fromUrl } from 'geotiff'; +import { fromUrl as fromGeotiffUrl } from 'geotiff'; import { isValidURL } from '../../utils/URLUtils'; import ConfigUtils from '../../utils/ConfigUtils'; @@ -57,8 +58,25 @@ export const getProjectionFromGeoKeys = (image) => { return null; }; +const abortError = (reject) => reject(new DOMException("Aborted", "AbortError")); +/** + * fromUrl with abort fetching of data and data slices + * Note: The abort action will not cancel data fetch request but just the promise, + * because of the issue in https://github.com/geotiffjs/geotiff.js/issues/408 + */ +const fromUrl = (url, signal) => { + if (signal?.aborted) { + return abortError(Promise.reject); + } + return new Promise((resolve, reject) => { + signal?.addEventListener("abort", () => abortError(reject)); + return fromGeotiffUrl(url) + .then((image)=> image.getImage()) // Fetch and read first image to get medatadata of the tif + .then((image) => resolve(image)) + .catch(()=> abortError(reject)); + }); +}; let capabilitiesCache = {}; - export const getRecords = (_url, startPosition, maxRecords, text, info = {}) => { const service = get(info, 'options.service'); let layers = []; @@ -73,29 +91,43 @@ export const getRecords = (_url, startPosition, maxRecords, text, info = {}) => sources: [{url}], options: service.options || {} }; - if (service.fetchMetadata) { + const controller = get(info, 'options.controller'); + const isSave = get(info, 'options.save', false); + // Fetch metadata only on saving the service (skip on search) + if ((isNil(service.fetchMetadata) || service.fetchMetadata) && isSave) { const cached = capabilitiesCache[url]; if (cached && new Date().getTime() < cached.timestamp + (ConfigUtils.getConfigProp('cacheExpire') || 60) * 1000) { return {...cached.data}; } - return fromUrl(url) - .then(geotiff => geotiff.getImage()) + return fromUrl(url, controller?.signal) .then(image => { const crs = getProjectionFromGeoKeys(image); const extent = image.getBoundingBox(); const isProjectionDefined = isProjectionAvailable(crs); layer = { ...layer, + sourceMetadata: { + crs, + extent: extent, + width: image.getWidth(), + height: image.getHeight(), + tileWidth: image.getTileWidth(), + tileHeight: image.getTileHeight(), + origin: image.getOrigin(), + resolution: image.getResolution() + }, // skip adding bbox when geokeys or extent is empty - ...(!isEmpty(extent) && !isEmpty(crs) && isProjectionDefined && { + ...(!isEmpty(extent) && !isEmpty(crs) && { bbox: { crs, - bounds: { - minx: extent[0], - miny: extent[1], - maxx: extent[2], - maxy: extent[3] - } + ...(isProjectionDefined && { + bounds: { + minx: extent[0], + miny: extent[1], + maxx: extent[2], + maxy: extent[3] + }} + ) } }) }; diff --git a/web/client/components/catalog/CatalogServiceEditor.jsx b/web/client/components/catalog/CatalogServiceEditor.jsx index b4e747e75b..5eb06219a5 100644 --- a/web/client/components/catalog/CatalogServiceEditor.jsx +++ b/web/client/components/catalog/CatalogServiceEditor.jsx @@ -17,7 +17,20 @@ import Message from "../I18N/Message"; import AdvancedSettings from './editor/AdvancedSettings'; import MainForm from './editor/MainForm'; -export default ({ +const withAbort = (Component) => { + return (props) => { + const [abortController, setAbortController] = useState(null); + const onSave = () => { + // Currently abort request on saving is applicable only for COG service + const controller = props.format === 'cog' ? new AbortController() : null; + setAbortController(controller); + return props.onAddService({save: true, controller}); + }; + const onCancel = () => abortController && props.saving ? abortController?.abort() : props.onChangeCatalogMode("view"); + return ; + }; +}; +const CatalogServiceEditor = ({ service = { title: "", type: "wms", @@ -39,9 +52,9 @@ export default ({ onChangeServiceProperty = () => {}, onToggleTemplate = () => {}, onToggleThumbnail = () => {}, - onAddService = () => {}, onDeleteService = () => {}, - onChangeCatalogMode = () => {}, + onCancel = () => {}, + onSaveService = () => {}, onFormatOptionsFetch = () => {}, selectedService, isLocalizedLayerStylesEnabled, @@ -50,7 +63,8 @@ export default ({ layerOptions = {}, infoFormatOptions, services, - autoSetVisibilityLimits = false + autoSetVisibilityLimits = false, + disabled }) => { const [valid, setValid] = useState(true); return ( - {service && !service.isNew - ? : null } - @@ -110,3 +124,5 @@ export default ({ ); }; + +export default withAbort(CatalogServiceEditor); diff --git a/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx b/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx index 05e2d96463..f2f16f1bee 100644 --- a/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx +++ b/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx @@ -8,6 +8,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import expect from 'expect'; +import TestUtils from 'react-dom/test-utils'; import CatalogServiceEditor from '../CatalogServiceEditor'; import {defaultPlaceholder} from "../editor/MainFormUtils"; @@ -149,4 +150,93 @@ describe('Test CatalogServiceEditor', () => { let placeholder = defaultPlaceholder(service); expect(placeholder).toBe("e.g. https://mydomain.com/geoserver/wms"); }); + it('test save and delete button when saving', () => { + ReactDOM.render(, document.getElementById("container")); + let buttons = document.querySelectorAll('.form-group button'); + let saveBtn; let deleteBtn; + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + buttons.forEach(btn => {if (btn.textContent === 'catalog.delete') deleteBtn = btn;}); + expect(saveBtn).toBeTruthy(); + expect(deleteBtn).toBeTruthy(); + expect(saveBtn.classList.contains("disabled")).toBeTruthy(); + expect(deleteBtn.classList.contains("disabled")).toBeTruthy(); + }); + it('test saving service for COG type', () => { + const actions = { + onAddService: () => {} + }; + const spyOnAdd = expect.spyOn(actions, 'onAddService'); + ReactDOM.render(, document.getElementById("container")); + let buttons = document.querySelectorAll('.form-group button'); + let saveBtn; + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + expect(saveBtn).toBeTruthy(); + TestUtils.Simulate.click(saveBtn); + expect(spyOnAdd).toHaveBeenCalled(); + let arg = spyOnAdd.calls[0].arguments[0]; + expect(arg.save).toBe(true); + expect(arg.controller).toBeTruthy(); + + ReactDOM.render(, document.getElementById("container")); + buttons = document.querySelectorAll('.form-group button'); + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + expect(saveBtn).toBeTruthy(); + TestUtils.Simulate.click(saveBtn); + expect(spyOnAdd).toHaveBeenCalled(); + arg = spyOnAdd.calls[1].arguments[0]; + expect(arg.save).toBeTruthy(); + expect(arg.controller).toBeFalsy(); + }); + it('test cancel service', () => { + const actions = { + onChangeCatalogMode: () => {}, + onAddService: () => {} + }; + const spyOnCancel = expect.spyOn(actions, 'onChangeCatalogMode'); + ReactDOM.render(, document.getElementById("container")); + let buttons = document.querySelectorAll('.form-group button'); + let cancelBtn; + buttons.forEach(btn => {if (btn.textContent === 'cancel') cancelBtn = btn;}); + expect(cancelBtn).toBeTruthy(); + TestUtils.Simulate.click(cancelBtn); + expect(spyOnCancel).toHaveBeenCalled(); + let arg = spyOnCancel.calls[0].arguments[0]; + expect(arg).toBe('view'); + + const spyOnAdd = expect.spyOn(actions, 'onAddService'); + ReactDOM.render(, document.getElementById("container")); + buttons = document.querySelectorAll('.form-group button'); + let saveBtn; + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + TestUtils.Simulate.click(saveBtn); + expect(spyOnAdd).toHaveBeenCalled(); + + ReactDOM.render(, document.getElementById("container")); + buttons = document.querySelectorAll('.form-group button'); + buttons.forEach(btn => {if (btn.textContent === 'cancel') cancelBtn = btn;}); + TestUtils.Simulate.click(cancelBtn); + expect(spyOnCancel.calls[1]).toBeFalsy(); + }); }); diff --git a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx index 048fd385ae..45900f3d36 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx @@ -58,7 +58,7 @@ export default ({ onChangeServiceProperty("fetchMetadata", e.target.checked)} - checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : false}> + checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : true}>  } /> diff --git a/web/client/epics/catalog.js b/web/client/epics/catalog.js index 3545be647d..294b54b8a1 100644 --- a/web/client/epics/catalog.js +++ b/web/client/epics/catalog.js @@ -292,7 +292,7 @@ export default (API) => ({ */ newCatalogServiceAdded: (action$, store) => action$.ofType(ADD_SERVICE) - .switchMap(() => { + .switchMap(({options} = {}) => { const state = store.getState(); const newService = newServiceSelector(state); const maxRecords = pageSizeSelector(state); @@ -310,7 +310,7 @@ export default (API) => ({ startPosition: 1, maxRecords, text: "", - options: {service, isNewService: true} + options: {service, isNewService: true, ...options} }) ); }) diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 286fa3bb6c..ef1b2ebb39 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -1534,7 +1534,7 @@ "tooltip": "Fügen Sie der Karte Ebenen hinzu", "autoload": "Suche in Dienstauswahl", "fetchMetadata": { - "label": "Laden Sie Dateimetadaten bei der Suche herunter", + "label": "Dateimetadaten beim Speichern herunterladen", "tooltip": "Diese Option ruft Metadaten ab, um das Zoomen auf Ebene zu unterstützen. Es kann den Suchvorgang verlangsamen, wenn die Bilder zu groß oder zu viele sind." }, "clearValueText": "Auswahl aufheben", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 60abe63d93..5cb1bc83a7 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -1495,7 +1495,7 @@ "tooltip": "Add layers to the map", "autoload": "Search on service selection", "fetchMetadata": { - "label": "Download file metadata on search", + "label": "Download file metadata on save", "tooltip": "This option will fetch metadata to support the zoom to layer. It may slow down the search operation if the images are too big or too many." }, "clearValueText": "Clear selection", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index a36bc8efa3..be1ec9351b 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -1496,7 +1496,7 @@ "tooltip": "agregar capas al mapa", "autoload": "Buscar en la selección de servicios", "fetchMetadata": { - "label": "Descargar metadatos de archivos en la búsqueda", + "label": "Descargar metadatos del archivo al guardar", "tooltip": "Esta opción recuperará metadatos para admitir el zoom a la capa. Puede ralentizar la operación de búsqueda si las imágenes son demasiado grandes o demasiadas." }, "clearValueText": "Borrar selección", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index e6b3477ca1..1999e81991 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -1496,7 +1496,7 @@ "tooltip": "Ajouter des couches à la carte", "autoload": "Recherche sur la sélection du service", "fetchMetadata": { - "label": "Télécharger les métadonnées du fichier lors de la recherche", + "label": "Télécharger les métadonnées du fichier lors de l'enregistrement", "tooltip": "Cette option récupérera les métadonnées pour prendre en charge le zoom sur la couche. Cela peut ralentir l'opération de recherche si les images sont trop grandes ou trop nombreuses." }, "clearValueText": "Effacer la sélection", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 9422ce5ec9..cb100fe986 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -1494,7 +1494,7 @@ "title": "Catalogo", "autoload": "Ricerca alla selezione del servizio", "fetchMetadata": { - "label": "Scarica i metadati dei file durante la ricerca", + "label": "Scarica i metadati del file al salvataggio", "tooltip": "Questa opzione recupererà i metadati per supportare lo zoom a livello. Potrebbe rallentare l'operazione di ricerca se le immagini sono troppo grandi o troppe." }, "clearValueText": "Cancella selezione",