diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1bda044..c4a153e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,10 +16,10 @@ jobs: steps: - name: Checkout the repo uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: '3.8' - name: Run pre-commit action uses: pre-commit/action@v2.0.0 build: @@ -34,16 +34,16 @@ jobs: - name: Install Python uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: '3.8' architecture: 'x64' - name: Setup pip cache uses: actions/cache@v2 with: path: ~/.cache/pip - key: pip-3.7-${{ hashFiles('package.json') }} + key: pip-3.8-${{ hashFiles('package.json') }} restore-keys: | - pip-3.7- + pip-3.8- pip- - name: Get yarn cache directory path diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bc30176..6fa1269 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,10 +15,10 @@ jobs: - name: Checkout source uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: '3.8' - name: Check that the current version isn't already on PyPi run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c6e0b1b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: Run Tests + +on: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + schedule: + - cron: '0 7 * * 1' + workflow_dispatch: + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8, '3.11'] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + + - name: Setup pip cache + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: pip-${{ matrix.python-version }}-${{ hashFiles('package.json') }} + restore-keys: | + pip-${{ matrix.python-version }}- + pip- + + - name: Install the extension + run: python -m pip install .[dev] + - name: Test with pytest + run: pytest -v diff --git a/CHANGES.md b/CHANGES.md index 46fdf6d..dd24ccd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Release history +## 3.5.0 (2024-02-08) + +- New `provider` filtering [(#127)](https://github.com/CS-SI/eodag-labextension/pull/127) +- New python tests [(#127)](https://github.com/CS-SI/eodag-labextension/pull/127)[(#130)](https://github.com/CS-SI/eodag-labextension/pull/130) +- Product type `title` instead of longer `abstract` displayed in dropdown list tooltip [(#131)](https://github.com/CS-SI/eodag-labextension/pull/131) +- Supported python versions starting from `3.8` to `3.11` [(#132)](https://github.com/CS-SI/eodag-labextension/pull/132) + ## 3.4.0 (2023-10-12) - Updates internal geometry format and removes exposed REST API [(#120)](https://github.com/CS-SI/eodag-labextension/pull/120) diff --git a/README.md b/README.md index ca73f56..56c9cfc 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,11 @@ Click on this icon in the left of JupyterLab interface to open EODAG-Labextensio With displayed search form, you can enter search extent and following search criteria: +- **Provider**: the provider on which to perform the search. If no provider is selected, search will loop on providers + by [priority](https://eodag.readthedocs.io/en/stable/getting_started_guide/configure.html#priority-setting), and + return the first non empty results. - **Product type**: the searched product type. List filtering is performed using product types description keywords. - For each entry of the drop-down list, a tooltip is displayed at hovering time with corresponding description. + For each entry of the drop-down list, a tooltip is displayed at hovering time with corresponding title.  - **Date range**: minimal and maximal dates of the search temporal window. diff --git a/eodag_labextension/handlers.py b/eodag_labextension/handlers.py index e0fa6b6..7f9b454 100644 --- a/eodag_labextension/handlers.py +++ b/eodag_labextension/handlers.py @@ -4,29 +4,100 @@ """Tornado web requests handlers""" -import json import re +import orjson import tornado -from eodag.rest.utils import eodag_api, get_product_types, search_products +from eodag.rest.utils import eodag_api, search_products from eodag.utils import parse_qs -from eodag.utils.exceptions import AuthenticationError, NoMatchingProductType, UnsupportedProductType, ValidationError +from eodag.utils.exceptions import ( + AuthenticationError, + NoMatchingProductType, + UnsupportedProductType, + UnsupportedProvider, + ValidationError, +) from jupyter_server.base.handlers import APIHandler from jupyter_server.utils import url_path_join class ProductTypeHandler(APIHandler): - """Product type listing handler + """Product type listing handlerd""" + + @tornado.web.authenticated + def get(self): + """Get endpoint""" + query_dict = parse_qs(self.request.query) + + provider = None + if "provider" in query_dict and isinstance(query_dict["provider"], list) and len(query_dict["provider"]) > 0: + provider = query_dict.pop("provider")[0] + provider = None if not provider or provider == "null" else provider + + try: + product_types = eodag_api.list_product_types(provider=provider) + except UnsupportedProvider as e: + self.set_status(400) + self.finish({"error": str(e)}) + return + + self.write(orjson.dumps(product_types)) - .. note:: - Product types endpoint filtered by provider not implemented""" +class ProvidersHandler(APIHandler): + """Providers listing handler""" @tornado.web.authenticated def get(self): """Get endpoint""" - self.write(json.dumps(get_product_types())) + available_providers_kwargs = {} + query_dict = parse_qs(self.request.query) + if ( + "product_type" in query_dict + and isinstance(query_dict["product_type"], list) + and len(query_dict["product_type"]) > 0 + ): + available_providers_kwargs["product_type"] = query_dict["product_type"][0] + available_providers = eodag_api.available_providers(**available_providers_kwargs) + + all_providers_list = [ + dict( + provider=provider, + priority=conf.priority, + description=getattr(conf, "description", None), + url=getattr(conf, "url", None), + ) + for provider, conf in eodag_api.providers_config.items() + if provider in available_providers + ] + all_providers_list.sort(key=lambda x: (x["priority"] * -1, x["provider"])) + + returned_providers = [] + if "keywords" in query_dict and isinstance(query_dict["keywords"], list) and len(query_dict["keywords"]) > 0: + # 1. List providers starting with given keyword + first_keyword = query_dict["keywords"][0].lower() + returned_providers = [p for p in all_providers_list if p["provider"].lower().startswith(first_keyword)] + providers_ids = [p["provider"] for p in returned_providers] + + # 2. List providers containing given keyword + returned_providers += [ + p + for p in all_providers_list + if first_keyword in p["provider"].lower() and p["provider"] not in providers_ids + ] + providers_ids = [p["provider"] for p in returned_providers] + + # 3. List providers containing given keyword in decription + returned_providers += [ + p + for p in all_providers_list + if first_keyword in p["description"].lower() and p["provider"] not in providers_ids + ] + else: + returned_providers = all_providers_list + + self.write(orjson.dumps(returned_providers)) class GuessProductTypeHandler(APIHandler): @@ -37,23 +108,59 @@ def get(self): """Get endpoint""" query_dict = parse_qs(self.request.query) - guess_kwargs = {} - # ["aa bb", "cc-dd_ee"] to "*aa* *bb* *cc* **dd* *ee*" - for k, v in query_dict.items(): - guess_kwargs[k] = re.sub(r"(\S+)", r"*\1*", " ".join(v).replace("-", " ").replace("_", " ")) + provider = None + if "provider" in query_dict and isinstance(query_dict["provider"], list) and len(query_dict["provider"]) > 0: + provider = query_dict.pop("provider")[0] + provider = None if not provider or provider == "null" else provider try: - # guessed product types ids - guessed_ids_list = eodag_api.guess_product_type(**guess_kwargs) - # product types with full associated metadata - guessed_list = [ - dict({"ID": k}, **v) for k, v in eodag_api.product_types_config.source.items() if k in guessed_ids_list - ] - - self.write(json.dumps(guessed_list)) + returned_product_types = [] + # fetch all product types + all_product_types = eodag_api.list_product_types(provider=provider) + + if ( + "keywords" in query_dict + and isinstance(query_dict["keywords"], list) + and len(query_dict["keywords"]) > 0 + ): + # 1. List product types starting with given keywords + first_keyword = query_dict["keywords"][0].lower() + returned_product_types = [pt for pt in all_product_types if pt["ID"].lower().startswith(first_keyword)] + returned_product_types_ids = [pt["ID"] for pt in returned_product_types] + + # 2. List product types containing keyword + returned_product_types += [ + pt + for pt in all_product_types + if first_keyword in pt["ID"].lower() and pt["ID"] not in returned_product_types_ids + ] + returned_product_types_ids += [pt["ID"] for pt in returned_product_types] + + # 3. Append guessed product types + guess_kwargs = {} + # ["aa bb", "cc-dd_ee"] to "*aa* *bb* *cc* **dd* *ee*" + for k, v in query_dict.items(): + guess_kwargs[k] = re.sub(r"(\S+)", r"*\1*", " ".join(v).replace("-", " ").replace("_", " ")) + + # guessed product types ids + guessed_ids_list = eodag_api.guess_product_type(**guess_kwargs) + # product types with full associated metadata + returned_product_types += [ + pt + for pt in all_product_types + if pt["ID"] in guessed_ids_list and pt["ID"] not in returned_product_types_ids + ] + else: + returned_product_types = all_product_types + + self.write(orjson.dumps(returned_product_types)) except NoMatchingProductType: - self.write(json.dumps([])) + self.write(orjson.dumps([])) + except UnsupportedProvider as e: + self.set_status(400) + self.finish({"error": str(e)}) + return class SearchHandler(APIHandler): @@ -63,13 +170,17 @@ class SearchHandler(APIHandler): def post(self, product_type): """Post endpoint""" - arguments = json.loads(self.request.body) + arguments = orjson.loads(self.request.body) # move geom to intersects parameter geom = arguments.pop("geom", None) if geom: arguments["intersects"] = geom + provider = arguments.pop("provider", None) + if provider and provider != "null": + arguments["provider"] = provider + try: response = search_products(product_type, arguments, stac_formatted=False) except ValidationError as e: @@ -121,12 +232,14 @@ def setup_handlers(web_app, url_path): # matching patterns host_pattern = ".*$" product_types_pattern = url_path_join(base_url, url_path, "product-types") + providers_pattern = url_path_join(base_url, url_path, "providers") guess_product_types_pattern = url_path_join(base_url, url_path, "guess-product-type") search_pattern = url_path_join(base_url, url_path, r"(?P<product_type>[\w-]+)") # handlers added for each pattern handlers = [ (product_types_pattern, ProductTypeHandler), + (providers_pattern, ProvidersHandler), (guess_product_types_pattern, GuessProductTypeHandler), (MethodAndPathMatch("POST", search_pattern), SearchHandler), ] diff --git a/notebooks/images/eodag_labext_form.png b/notebooks/images/eodag_labext_form.png index 9fbdcd9..e53781c 100644 Binary files a/notebooks/images/eodag_labext_form.png and b/notebooks/images/eodag_labext_form.png differ diff --git a/notebooks/images/eodag_labext_product_types.png b/notebooks/images/eodag_labext_product_types.png index 5e5176a..362467f 100644 Binary files a/notebooks/images/eodag_labext_product_types.png and b/notebooks/images/eodag_labext_product_types.png differ diff --git a/notebooks/user_manual.ipynb b/notebooks/user_manual.ipynb index a1396bb..d0365f9 100644 --- a/notebooks/user_manual.ipynb +++ b/notebooks/user_manual.ipynb @@ -71,7 +71,10 @@ "\n", "With displayed search form, you can enter search extent and following search criteria:\n", "\n", - "* **Product type**: the searched product type. List filtering is performed using product types description keywords. For each entry of the drop-down list, a tooltip is displayed at hovering time with corresponding description.\n", + "* **Provider**: the provider on which to perform the search. If no provider is selected, search will loop on providers\n", + " by [priority](https://eodag.readthedocs.io/en/stable/getting_started_guide/configure.html#priority-setting), and\n", + " return the first non empty results.\n", + "* **Product type**: the searched product type. List filtering is performed using product types description keywords. For each entry of the drop-down list, a tooltip is displayed at hovering time with corresponding title.\n", "<img style=\"display: block;\" src=\"https://raw.githubusercontent.com/CS-SI/eodag-labextension/develop/notebooks/images/eodag_labext_product_types.png\" alt=\"product types\">\n", "\n", "* **Date range**: minimal and maximal dates of the search temporal window.\n", @@ -265,7 +268,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.10.12" }, "widgets": { "application/vnd.jupyter.widget-state+json": { diff --git a/package.json b/package.json index cfdd13c..c4a2dc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eodag-labextension", - "version": "3.4.0", + "version": "3.5.0", "description": "Searching remote sensed imagery from various image providers", "keywords": [ "jupyter", diff --git a/setup.py b/setup.py index dbcddad..f49951c 100644 --- a/setup.py +++ b/setup.py @@ -62,21 +62,22 @@ "tornado>=6.0.3,<7.0.0", "notebook>=6.0.3,<7.0.0", "eodag[notebook]>=2.8.0", + "orjson", ], - extras_require={"dev": ["black", "pre-commit"]}, + extras_require={"dev": ["black", "pre-commit", "pytest"]}, zip_safe=False, include_package_data=True, - python_requires=">=3.7", + python_requires=">=3.8", platforms="Linux, Mac OS X, Windows", keywords=["Jupyter", "JupyterLab", "JupyterLab3"], classifiers=[ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Framework :: Jupyter", ], ) diff --git a/src/Autocomplete.tsx b/src/Autocomplete.tsx index 9b62b7d..53f8a8a 100644 --- a/src/Autocomplete.tsx +++ b/src/Autocomplete.tsx @@ -14,11 +14,6 @@ import ReactTooltip from 'react-tooltip'; import { OptionTypeBase } from 'react-select/src/types'; import AsyncSelect from 'react-select/async'; -import { showErrorMessage } from '@jupyterlab/apputils'; -import { URLExt } from '@jupyterlab/coreutils'; -import { ServerConnection } from '@jupyterlab/services'; -import { EODAG_SERVER_ADRESS } from './config'; -import { map } from 'lodash'; import { IOptionTypeBase } from './FormComponent'; function NoOptionsMessage(props: any) { @@ -92,57 +87,30 @@ interface IProps { suggestions: OptionTypeBase[]; value: string; handleChange: any; + label: string; + placeholder?: string; + loadSuggestions?: (inputValue: string) => Promise<IOptionTypeBase[]>; } class IntegrationReactSelect extends React.Component<IProps> { render() { - const { suggestions, value, handleChange } = this.props; + const { + label, + suggestions, + value, + handleChange, + placeholder, + loadSuggestions + } = this.props; + const currentValue: OptionTypeBase = value ? suggestions.find(e => e.value === value) : undefined; - const guessProductTypes = async (inputValue: string) => { - const _serverSettings = ServerConnection.makeSettings(); - const _eodag_server = URLExt.join( - _serverSettings.baseUrl, - `${EODAG_SERVER_ADRESS}` - ); - - return fetch( - URLExt.join(_eodag_server, `guess-product-type?keywords=${inputValue}`), - { - credentials: 'same-origin' - } - ) - .then(response => { - if (response.status >= 400) { - showErrorMessage( - `Unable to contact the EODAG server. Are you sure the adress is ${_eodag_server}/ ?`, - {} - ); - throw new Error('Bad response from server'); - } - return response.json(); - }) - .then(products => { - const guessProductTypes = map(products, product => ({ - value: product.ID, - label: product.ID, - description: product.abstract - })); - return guessProductTypes; - }); - }; - - const loadSuggestions = (inputValue: string) => - new Promise<IOptionTypeBase[]>(resolve => { - resolve(guessProductTypes(inputValue)); - }); - return ( <div className="jp-EodagWidget-field"> <label className="jp-EodagWidget-input-name"> - Product type + {label} <div style={{ marginTop: 10 @@ -151,13 +119,12 @@ class IntegrationReactSelect extends React.Component<IProps> { <AsyncSelect className="jp-EodagWidget-select" classNamePrefix="jp-EodagWidget-select" - cacheOptions defaultOptions={suggestions} loadOptions={loadSuggestions} components={listcomponents} value={currentValue} onChange={handleChange} - placeholder="S2_..." + placeholder={placeholder} isClearable /> </div> diff --git a/src/CodeGenerator.ts b/src/CodeGenerator.ts index 1d7cad9..1c43f24 100644 --- a/src/CodeGenerator.ts +++ b/src/CodeGenerator.ts @@ -17,7 +17,8 @@ const formatCode = ( productType, geometry, cloud, - additionnalParameters + additionnalParameters, + provider }: IFormInput, replaceCode: boolean ) => { @@ -44,7 +45,12 @@ ${standardMessage}` geometry = "${geojsonToWKT(geometry)}"`; } code += ` -search_results, total_count = dag.search( +search_results, total_count = dag.search(`; + if (provider) { + code += ` + provider="${provider}",`; + } + code += ` productType="${productType}",`; if (geometryIsOk) { code += ` diff --git a/src/FormComponent.tsx b/src/FormComponent.tsx index 0f3bd26..573a727 100644 --- a/src/FormComponent.tsx +++ b/src/FormComponent.tsx @@ -12,14 +12,10 @@ import { UseFormReturn } from 'react-hook-form'; import { showErrorMessage } from '@jupyterlab/apputils'; -import { URLExt } from '@jupyterlab/coreutils'; -import { ServerConnection } from '@jupyterlab/services'; -import { map } from 'lodash'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import 'isomorphic-fetch'; import Autocomplete from './Autocomplete'; -import { EODAG_SERVER_ADRESS } from './config'; import SearchService from './SearchService'; import { ChangeEvent } from 'react'; import MapExtentComponent from './MapExtentComponent'; @@ -36,6 +32,7 @@ import { } from './icones.js'; import ReactTooltip from 'react-tooltip'; import { ThreeDots } from 'react-loader-spinner'; +import { useFetchProduct, useFetchProvider } from './hooks/useFetchData'; export interface IProps { handleShowFeature: any; @@ -44,10 +41,21 @@ export interface IProps { isNotebookCreated: any; commands: any; } + export interface IOptionTypeBase { [key: string]: any; } +export interface IProduct { + ID: string; + title: string; +} + +export interface IProvider { + provider: string; + description: string; +} + export const FormComponent: FC<IProps> = ({ handleShowFeature, saveFormValues, @@ -56,6 +64,7 @@ export const FormComponent: FC<IProps> = ({ commands }) => { const [productTypes, setProductTypes] = useState<IOptionTypeBase[]>(); + const [providers, setProviders] = useState<IOptionTypeBase[]>(); const defaultStartDate: Date = undefined; const defaultEndDate: Date = undefined; const [startDate, setStartDate] = useState(undefined); @@ -63,7 +72,8 @@ export const FormComponent: FC<IProps> = ({ const [cloud, setCloud] = useState(100); const [isLoadingSearch, setIsLoadingSearch] = useState(false); const [openModal, setOpenModal] = useState(true); - const [selectValue, setSelectValue] = useState(null); + const [providerValue, setProviderValue] = useState(null); + const [productTypeValue, setProductTypeValue] = useState(null); const { control, @@ -81,53 +91,24 @@ export const FormComponent: FC<IProps> = ({ }); useEffect(() => { - // Fetch product types - const _serverSettings = ServerConnection.makeSettings(); - const _eodag_server = URLExt.join( - _serverSettings.baseUrl, - `${EODAG_SERVER_ADRESS}` - ); - fetch(URLExt.join(_eodag_server, 'product-types/'), { - credentials: 'same-origin' - }) - .then(response => { - if (response.status >= 400) { - showErrorMessage( - `Unable to contact the EODAG server. Are you sure the adress is ${_eodag_server}/ ?`, - {} - ); - throw new Error('Bad response from server'); - } - return response.json(); - }) - .then(products => { - const productTypes = map(products, product => ({ - value: product.ID, - label: product.ID, - description: product.abstract - })); - setProductTypes(productTypes); - }) - .catch(() => { - showErrorMessage( - `Unable to contact the EODAG server. Are you sure the adress is ${_eodag_server}/ ?`, - {} - ); - }); - }, []); + const fetchData = async () => { + const fetchProduct = useFetchProduct(); + const productList = await fetchProduct(providerValue); + setProductTypes(productList); + }; - // useEffect( - // () => { - // if (!_.isEmpty(errors)) { - // showErrorMessage( - // 'The following fields are required', - // _.keys(errors).join(', ') - // ).then(() => clearErrors()); - // } - // }, - // // useEffect is not triggered with only errors as dependency thus we need to list all its elements - // [errors] - // ); + fetchData(); + }, [providerValue]); + + useEffect(() => { + const fetchData = async () => { + const fetchProvider = useFetchProvider(); + const providerList = await fetchProvider(productTypeValue); + setProviders(providerList); + }; + + fetchData(); + }, [productTypeValue]); const onSubmit: SubmitHandler<IFormInput> = data => { if (!isNotebookCreated()) { @@ -168,6 +149,9 @@ export const FormComponent: FC<IProps> = ({ commands.execute('settingeditor:open', { query: 'EODAG' }); }; + const loadProductTypesSuggestions = useFetchProduct(); + const loadProviderSuggestions = useFetchProvider(); + return ( <div className="jp-EodagWidget-wrapper"> <form onSubmit={handleSubmit(onSubmit)} className="jp-EodagWidget-form"> @@ -182,17 +166,41 @@ export const FormComponent: FC<IProps> = ({ /> </div> <div className="jp-EodagWidget-field"> + <Controller + name="provider" + control={control} + render={({ field: { onChange, value } }) => ( + <Autocomplete + label="Provider" + placeholder="Any" + suggestions={providers} + value={value} + loadSuggestions={(inputValue: string) => + loadProviderSuggestions(null, inputValue) + } + handleChange={(e: IOptionTypeBase | null) => { + onChange(e?.value); + setProviderValue(e?.value); + }} + /> + )} + /> <Controller name="productType" control={control} rules={{ required: true }} render={({ field: { onChange, value } }) => ( <Autocomplete + label="Product Type" suggestions={productTypes} + placeholder="S2_..." value={value} + loadSuggestions={(inputValue: string) => + loadProductTypesSuggestions(providerValue, inputValue) + } handleChange={(e: IOptionTypeBase | null) => { onChange(e?.value); - setSelectValue(e?.value); + setProductTypeValue(e?.value); }} /> )} @@ -312,7 +320,7 @@ export const FormComponent: FC<IProps> = ({ type="submit" color="primary" className={ - !selectValue + !productTypeValue ? 'jp-EodagWidget-buttons-button jp-EodagWidget-buttons-button__disabled' : 'jp-EodagWidget-buttons-button' } @@ -327,7 +335,7 @@ export const FormComponent: FC<IProps> = ({ <br /> Results </p> - {!selectValue && ( + {!productTypeValue && ( <ReactTooltip id="btn-preview-results" className="jp-Eodag-tooltip" @@ -343,7 +351,7 @@ export const FormComponent: FC<IProps> = ({ type="submit" color="primary" className={ - !selectValue + !productTypeValue ? 'jp-EodagWidget-buttons-button jp-EodagWidget-buttons-button__disabled' : 'jp-EodagWidget-buttons-button' } @@ -358,7 +366,7 @@ export const FormComponent: FC<IProps> = ({ <br /> Code </p> - {!selectValue && ( + {!productTypeValue && ( <ReactTooltip id="btn-generate-value" className="jp-Eodag-tooltip" diff --git a/src/SearchService.ts b/src/SearchService.ts index 91b3cae..59c4efa 100644 --- a/src/SearchService.ts +++ b/src/SearchService.ts @@ -41,7 +41,8 @@ class SearchService { dtend: formValues.endDate ? formatDate(formValues.endDate) : undefined, cloudCover: formValues.cloud < 100 ? formValues.cloud : undefined, page: page, - geom: formValues.geometry + geom: formValues.geometry, + provider: formValues.provider }; if (formValues.additionnalParameters) { diff --git a/src/hooks/useFetchData.ts b/src/hooks/useFetchData.ts new file mode 100644 index 0000000..45ac789 --- /dev/null +++ b/src/hooks/useFetchData.ts @@ -0,0 +1,100 @@ +import { showErrorMessage } from '@jupyterlab/apputils'; +import { URLExt } from '@jupyterlab/coreutils'; +import { ServerConnection } from '@jupyterlab/services'; +import { EODAG_SERVER_ADRESS } from './../config'; +import { IOptionTypeBase, IProduct, IProvider } from './../FormComponent'; + +interface IFetchDataProps<T> { + queryParams: string; + onSuccess: (data: T[]) => Promise<IOptionTypeBase[]>; +} + +const fetchData = async <T>({ + queryParams, + onSuccess +}: IFetchDataProps<T>): Promise<IOptionTypeBase[]> => { + const serverSettings = ServerConnection.makeSettings(); + const eodagServer = URLExt.join( + serverSettings.baseUrl, + `${EODAG_SERVER_ADRESS}` + ); + + try { + const response = await fetch(URLExt.join(eodagServer, queryParams), { + credentials: 'same-origin' + }); + if (response.status >= 400) { + throw new Error('Bad response from server'); + } + const data = await response.json(); + return onSuccess(data); + } catch (error) { + showErrorMessage( + `Unable to contact the EODAG server. Are you sure the address is ${eodagServer}/ ?`, + {} + ); + return Promise.reject(error); + } +}; + +const useFetchProduct = () => { + const fetchProduct = async ( + providerValue: string, + inputValue?: string + ): Promise<IOptionTypeBase[]> => { + let queryParams = 'guess-product-type?'; + + if (inputValue) { + queryParams += `keywords=${inputValue}`; + } + + if (providerValue) { + queryParams += queryParams.includes('keywords') ? '&' : ''; + queryParams += `provider=${providerValue}`; + } + return fetchData<IProduct>({ + queryParams, + onSuccess: data => + Promise.resolve( + data.map((d: IProduct) => ({ + value: d.ID, + label: d.ID, + description: d.title + })) + ) + }); + }; + + return fetchProduct; +}; + +const useFetchProvider = () => { + const fetchProvider = async ( + productTypeValue: string, + inputValue?: string + ): Promise<IOptionTypeBase[]> => { + let queryParams = 'providers?'; + + if (inputValue) { + queryParams += `keywords=${inputValue}`; + } + if (productTypeValue) { + queryParams += `product_type=${productTypeValue}`; + } + return fetchData<IProvider>({ + queryParams, + onSuccess: data => + Promise.resolve( + data.map((d: IProvider) => ({ + value: d.provider, + label: d.provider, + description: d.description + })) + ) + }); + }; + + return fetchProvider; +}; + +export { fetchData, useFetchProduct, useFetchProvider }; diff --git a/src/types.ts b/src/types.ts index b9a715e..cadc6d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,7 @@ export interface IFormInput { startDate: Date; endDate: Date; productType: string; + provider: string; cloud: number; additionnalParameters?: { name: string; value: string }[]; } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d5c232e --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 CS GROUP - France, http://www.c-s.fr +# All rights reserved +"""EODAG-labextension tests""" diff --git a/tests/test_handlers.py b/tests/test_handlers.py new file mode 100644 index 0000000..3b25bd7 --- /dev/null +++ b/tests/test_handlers.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 CS GROUP - France, http://www.c-s.fr +# All rights reserved +import json +import os +import re +from unittest import mock + +from notebook.notebookapp import NotebookApp +from tornado.testing import AsyncHTTPTestCase +from tornado.web import authenticated + +from eodag_labextension import load_jupyter_server_extension +from eodag_labextension.handlers import APIHandler + + +class MockUser: + name = "test" + + +class TestEodagLabExtensionHandler(AsyncHTTPTestCase): + @classmethod + def setUpClass(cls): + # backup os.environ as it will be modified by tests + cls.eodag_env_pattern = re.compile(r"EODAG_\w+") + cls.eodag_env_backup = {k: v for k, v in os.environ.items() if cls.eodag_env_pattern.match(k)} + # disable product types fetch + os.environ["EODAG_EXT_PRODUCT_TYPES_CFG_FILE"] = "" + + @classmethod + def tearDownClass(cls): + super(TestEodagLabExtensionHandler, cls).tearDownClass() + # restore os.environ + for k, v in os.environ.items(): + if cls.eodag_env_pattern.match(k): + os.environ.pop(k) + os.environ.update(cls.eodag_env_backup) + + def get_app(self): + # Create a new NotebookApp instance + app = NotebookApp() + app.initialize(argv=[]) + + # Load extension + load_jupyter_server_extension(app) + + return app.web_app + + @mock.patch.object(APIHandler, "get_current_user", return_value=MockUser()) + @mock.patch.object(authenticated, "__call__", return_value=lambda x: x) + def fetch_results(self, url, mock_auth, mock_user): + """Check that request status is 200 and return the json result as dict""" + response = self.fetch(url) + self.assertEqual(response.code, 200) + return json.loads(response.body.decode("utf-8")) + + @mock.patch.object(APIHandler, "get_current_user", return_value=MockUser()) + @mock.patch.object(authenticated, "__call__", return_value=lambda x: x) + def fetch_results_err400(self, url, mock_auth, mock_user): + """Check that request returns a 400 error""" + response = self.fetch(url) + self.assertEqual(response.code, 400) + return json.loads(response.body.decode("utf-8")) + + def test_product_types(self): + # all product types + results = self.fetch_results("/eodag/product-types") + self.assertIn("S2_MSI_L1C", [pt["ID"] for pt in results]) + + # single provider product types + less_results = self.fetch_results("/eodag/product-types?provider=peps") + self.assertGreater(len(less_results), 0) + self.assertLess(len(less_results), len(results)) + + # unknown provider + self.fetch_results_err400("/eodag/product-types?provider=foo") + + def test_providers(self): + # all providers + results = self.fetch_results("/eodag/providers") + self.assertIn("peps", [res["provider"] for res in results]) + + less_results = self.fetch_results("/eodag/providers?product_type=S2_MSI_L1C") + self.assertGreater(len(less_results), 0) + self.assertLess(len(less_results), len(results)) + + result_with_name = self.fetch_results("/eodag/providers?keywords=peps") + self.assertEqual(len(result_with_name), 1) + self.assertEqual(result_with_name[0]["provider"], "peps") + + result_with_description = self.fetch_results("/eodag/providers?keywords=cop") + self.assertGreater(len(result_with_description), 2) + self.assertEqual(result_with_description[0]["provider"], "cop_ads") + + no_result = self.fetch_results("/eodag/providers?product_type=foo") + self.assertEqual(len(no_result), 0) + + def test_guess_product_types(self): + all_results = self.fetch_results("/eodag/guess-product-type") + self.assertIn("S2_MSI_L1C", [pt["ID"] for pt in all_results]) + + one_provider_results = self.fetch_results("/eodag/guess-product-type?provider=creodias") + self.assertLess(len(one_provider_results), len(all_results)) + self.assertIn("COP_DEM_GLO90_DGED", [pt["ID"] for pt in all_results]) + + one_result = self.fetch_results("/eodag/guess-product-type?keywords=S2_MSI_L1C") + self.assertEqual(len(one_result), 1) + self.assertEqual(one_result[0]["ID"], "S2_MSI_L1C") + + another_result = self.fetch_results("/eodag/guess-product-type?keywords=Sentinel2%20L1C") + self.assertEqual(len(another_result), 1) + self.assertEqual(another_result[0]["ID"], "S2_MSI_L1C") + + more_results = self.fetch_results("/eodag/guess-product-type?keywords=Sentinel") + self.assertGreater(len(more_results), 1) + self.assertLess(len(more_results), len(all_results)) + self.assertIn("S2_MSI_L1C", [pt["ID"] for pt in more_results]) + + less_results = self.fetch_results("/eodag/guess-product-type?keywords=Sentinel&provider=peps") + self.assertGreater(len(more_results), 1) + self.assertLess(len(less_results), len(more_results)) + self.assertEqual(less_results[0]["ID"], "S1_SAR_GRD") + + other_results = self.fetch_results("/eodag/guess-product-type?keywords=cop") + self.assertGreater(len(other_results), 1) + self.assertLess(len(other_results), len(all_results)) + self.assertTrue(other_results[0]["ID"].lower().startswith("cop")) + + self.fetch_results_err400("/eodag/guess-product-type?provider=foo") diff --git a/tsconfig.json b/tsconfig.json index e46e569..3157439 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "strict": true, "strictNullChecks": false, "target": "es2017", - "types": [] + "types": [], + "skipLibCheck": true }, - "include": ["src/*", "style/*"] + "include": ["src/**/*", "style/*"] } diff --git a/yarn.lock b/yarn.lock index a0fbb6e..897e380 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,20 +2,29 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6": +"@babel/code-frame@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== dependencies: "@babel/highlight" "^7.18.6" -"@babel/generator@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" - integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== dependencies: - "@babel/types" "^7.20.5" + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" "@babel/helper-annotate-as-pure@^7.16.0": @@ -25,25 +34,25 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== -"@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== dependencies: - "@babel/types" "^7.18.6" + "@babel/types" "^7.22.5" "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7": version "7.18.6" @@ -57,28 +66,33 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz#4796bb14961521f0f8715990bee2fb6e51ce21bf" integrity sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw== -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== dependencies: - "@babel/types" "^7.18.6" + "@babel/types" "^7.22.5" "@babel/helper-string-parser@^7.18.10": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": +"@babel/helper-validator-identifier@^7.18.6": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -88,10 +102,19 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.18.10", "@babel/parser@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" - integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== "@babel/plugin-syntax-jsx@^7.17.12": version "7.18.6" @@ -107,40 +130,31 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" - integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.18.10" - "@babel/types" "^7.18.10" + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" "@babel/traverse@^7.4.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" - integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.5" - "@babel/types" "^7.20.5" + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.18.10", "@babel/types@^7.19.0", "@babel/types@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" - integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - "@babel/types@^7.18.6": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.0.tgz#75f21d73d73dc0351f3368d28db73465f4814600" @@ -150,6 +164,15 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@blueprintjs/colors@^4.0.0-alpha.3": version "4.1.6" resolved "https://registry.yarnpkg.com/@blueprintjs/colors/-/colors-4.1.6.tgz#a2e3d02b40867b3770187c69a8514f113ab4dd85" @@ -398,6 +421,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" @@ -416,6 +444,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": version "0.3.15" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" @@ -424,6 +457,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@juggle/resize-observer@^3.3.1": version "3.4.0" resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" @@ -2381,7 +2422,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1: +chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==