diff --git a/.env.sample b/.env.sample index c8456f4b..0af0f2a8 100644 --- a/.env.sample +++ b/.env.sample @@ -11,10 +11,10 @@ REACT_APP_ONEDS_TENANT_KEY= REACT_APP_API_ROOT=https://planetarycomputer-staging.microsoft.com # Root URL for image function app endpoints -REACT_APP_IMAGE_API_ROOT= +REACT_APP_IMAGE_API_ROOT=https://planetarycomputer-staging.microsoft.com/api/f -# Subscription key for Azure Maps -REACT_APP_AZMAPS_KEY= +# Client Id for Azure Maps +REACT_APP_AZMAPS_CLIENT_ID=8f49b6d6-5845-4e20-9015-9630df1ca8d2 # URL for JHub cloned repo launch (including 'git-pull') REACT_APP_HUB_URL= diff --git a/.github/workflows/azure-static-web-apps-icy-meadow-0fc35e30f.yml b/.github/workflows/azure-static-web-apps-icy-meadow-0fc35e30f.yml index 8aa95de9..73e1e325 100644 --- a/.github/workflows/azure-static-web-apps-icy-meadow-0fc35e30f.yml +++ b/.github/workflows/azure-static-web-apps-icy-meadow-0fc35e30f.yml @@ -11,6 +11,8 @@ on: jobs: build_and_deploy_job: + permissions: + pull-requests: write environment: staging if: github.event_name == 'push' || (github.event_name == 'pull_request' && @@ -20,10 +22,9 @@ jobs: env: REACT_APP_API_ROOT: ${{ secrets.API_ROOT }} REACT_APP_IMAGE_API_ROOT: ${{ secrets.IMAGE_API_ROOT }} + REACT_APP_AZMAPS_CLIENT_ID: ${{ secrets.AZMAPS_CLIENT_ID }} REACT_APP_ONEDS_TENANT_KEY: ${{ secrets.ONEDS_TENANT_KEY }} - REACT_APP_AZMAPS_KEY: ${{ secrets.AZMAPS_KEY }} REACT_APP_HUB_URL: ${{ secrets.HUB_URL }} - REACT_APP_AUTH_URL: ${{ secrets.AUTH_URL }} steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/azure-static-web-apps-thankful-sand-0ed34c70f.yml b/.github/workflows/azure-static-web-apps-thankful-sand-0ed34c70f.yml index 042a3aa2..88b5273e 100644 --- a/.github/workflows/azure-static-web-apps-thankful-sand-0ed34c70f.yml +++ b/.github/workflows/azure-static-web-apps-thankful-sand-0ed34c70f.yml @@ -11,6 +11,8 @@ on: jobs: build_and_deploy_job: + permissions: + pull-requests: write environment: production if: github.event_name == 'push' || (github.event_name == 'pull_request' && @@ -20,9 +22,8 @@ jobs: env: REACT_APP_API_ROOT: ${{ secrets.API_ROOT }} REACT_APP_IMAGE_API_ROOT: ${{ secrets.IMAGE_API_ROOT }} - REACT_APP_JSLL_APP_ID: ${{ secrets.JSLL_APP_ID }} + REACT_APP_AZMAPS_CLIENT_ID: ${{ secrets.AZMAPS_CLIENT_ID }} REACT_APP_ONEDS_TENANT_KEY: ${{ secrets.ONEDS_TENANT_KEY }} - REACT_APP_AZMAPS_KEY: ${{ secrets.AZMAPS_KEY }} REACT_APP_HUB_URL: ${{ secrets.HUB_URL }} steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/azure-static-web-apps-wonderful-stone-06c70c70f.yml b/.github/workflows/azure-static-web-apps-wonderful-stone-06c70c70f.yml index 34183b2f..699c5537 100644 --- a/.github/workflows/azure-static-web-apps-wonderful-stone-06c70c70f.yml +++ b/.github/workflows/azure-static-web-apps-wonderful-stone-06c70c70f.yml @@ -11,6 +11,8 @@ on: jobs: build_and_deploy_job: + permissions: + pull-requests: write environment: test if: github.event_name == 'push' || (github.event_name == 'pull_request' && @@ -20,10 +22,9 @@ jobs: env: REACT_APP_API_ROOT: ${{ secrets.API_ROOT }} REACT_APP_IMAGE_API_ROOT: ${{ secrets.IMAGE_API_ROOT }} + REACT_APP_AZMAPS_CLIENT_ID: ${{ secrets.AZMAPS_CLIENT_ID }} REACT_APP_ONEDS_TENANT_KEY: ${{ secrets.ONEDS_TENANT_KEY }} - REACT_APP_AZMAPS_KEY: ${{ secrets.AZMAPS_KEY }} REACT_APP_HUB_URL: ${{ secrets.HUB_URL }} - REACT_APP_AUTH_URL: ${{ secrets.AUTH_URL }} steps: - uses: actions/checkout@v3 with: diff --git a/README.md b/README.md index c332178c..841f8a65 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,29 @@ First, copy `.env.sample` file to `.env`, and ensure the configuration values ar |`REACT_APP_API_ROOT`| | The root URL for the STAC API, either prod, staging or a local instance. If the URL ends in 'stac', this is a special case that is handled by replacing 'stac' with the target service, e.g. 'data' or 'sas' |`REACT_APP_TILER_ROOT`| Optional | The root URL for the data tiler API, if not hosted from the domain of the STAC API. |`REACT_APP_IMAGE_API_ROOT`| PC APIs pcfunc endpoint | The root URL for the image data API for animations. -|`REACT_APP_AZMAPS_KEY`| Retrieve from Azure Portal | The key used to authenticate the Azure Maps inset map on a dataset detail page. +|`REACT_APP_AZMAPS_CLIENT_ID`| Retrieve from Azure Portal | The Client ID used to authenticate against Azure Maps. |`REACT_APP_HUB_URL`| Optional. URL to root Hub instance | Used to enable a request to launch the Hub with a specific git hosted file. |`REACT_APP_ONEDS_TENANT_KEY`| Lookup at | Telemetry key (not needed for dev) |`REACT_APP_AUTH_URL`| Optional. URL to root pc-session-api instance | Used to enable login work. -Run `./scripts/server` to launch a development server. +Run `./scripts/server --api` to launch a development server with a local Azure Functions host running. + +#### Azure Maps + +In the local development setups, the Azure Maps token is generated using the local developer identity. Be sure to +`az login` and `az account set --subscription "Planetary Computer"` to ensure the correct token is generated. Your identity +will also need the "Azure Maps Search and Render Data Reader" permission, which can be set with: + +```sh +USER_NAME=$(az account show --query user.name -o tsv) +az role assignment create \ + --assignee "$USER_NAME" \ + --role "Azure Maps Search and Render Data Reader" \ + --scope "/subscriptions/9da7523a-cb61-4c3e-b1d4-afa5fc6d2da9/resourceGroups/pc-datacatalog-rg/providers/Microsoft.Maps/accounts/pc-datacatalog-azmaps" \ + --subscription "Planetary Computer" +``` + +Note, you may need to assign this role via an identity that has JIT admin privileges enabled against the Planetary Computer subscription. #### Developing against local STAC assets diff --git a/api/Dockerfile b/api/Dockerfile index 4db6b000..6dad46df 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,10 +1,19 @@ -FROM mcr.microsoft.com/azure-functions/python:4-python3.9 +FROM mcr.microsoft.com/azure-cli:cbl-mariner2.0 ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ AzureFunctionsJobHost__Logging__Console__IsEnabled=true +RUN tdnf install libicu unzip wget -y +RUN wget https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.5530/Azure.Functions.Cli.linux-x64.4.0.5530.zip +RUN mkdir -p /usr/local/lib/Azure.Functions.Cli +RUN unzip Azure.Functions.Cli.linux-x64.4.0.5530.zip -d /usr/local/lib/Azure.Functions.Cli +RUN chmod +x /usr/local/lib/Azure.Functions.Cli/func + +ENV PATH="/usr/local/lib/Azure.Functions.Cli:${PATH}" + +RUN python3 -m ensurepip --upgrade COPY requirements.txt / -RUN pip install -r /requirements.txt +RUN pip3 install -r /requirements.txt COPY requirements-dev.txt / -RUN pip install -r /requirements-dev.txt +RUN pip3 install -r /requirements-dev.txt diff --git a/api/README.md b/api/README.md index 9e670df5..71a81c33 100644 --- a/api/README.md +++ b/api/README.md @@ -12,12 +12,12 @@ To set appropriate configuration values for the Function app, copy the `local.se The `local.settings.json` file has the following keys in the Values section: -|Key|KeyVault Key|Purpose| -|---|---|---| -|`NotificationHook`| | URL to send Teams notification on new Account Request -|`AuthAdminUrl`| | URL to the PC ID admin page which contains the signup table. Used in the Teams notification message. -|`SignupUrl`| | URL to POST new user content to on submission -|`SignupToken` | `pc-id--request-auth-token` | Bearer token required to make the above POST request +| Key | KeyVault Key | Purpose | +|--------------------|-----------------------------|------------------------------------------------------------------------------------------------------| +| `NotificationHook` | | URL to send Teams notification on new Account Request | +| `AuthAdminUrl` | | URL to the PC ID admin page which contains the signup table. Used in the Teams notification message. | +| `SignupUrl` | | URL to POST new user content to on submission | +| `SignupToken` | `pc-id--request-auth-token` | Bearer token required to make the above POST request | ### Production diff --git a/api/map-token/__init__.py b/api/map-token/__init__.py new file mode 100644 index 00000000..12021dc9 --- /dev/null +++ b/api/map-token/__init__.py @@ -0,0 +1,38 @@ +import json +import logging +from typing import TypedDict + +import azure.functions as func + +from azure.identity import DefaultAzureCredential +from azure.core.exceptions import ClientAuthenticationError + +logger = logging.getLogger("api.maps-token") +# For performance, exclude checking options we know won't be used +credential = DefaultAzureCredential( + exclude_environment_credential=True, + exclude_developer_cli_credential=True, + exclude_powershell_credential=True, + exclude_visual_studio_code_credential=True, +) + + +class TokenResponse(TypedDict): + token: str + expires_on: int + + +def main(req: func.HttpRequest) -> func.HttpResponse: + + logger.debug("Python HTTP trigger function processed a request.") + try: + logger.debug("Getting azure maps token") + token = credential.get_token("https://atlas.microsoft.com/.default") + logger.debug("Token acquired") + + resp: TokenResponse = {"token": token.token, "expires_on": token.expires_on} + + return func.HttpResponse(status_code=200, body=json.dumps(resp)) + except ClientAuthenticationError: + logger.exception(f"Error getting azure maps token") + return func.HttpResponse("Error getting token", status_code=500) diff --git a/api/map-token/function.json b/api/map-token/function.json new file mode 100644 index 00000000..e75ca8c3 --- /dev/null +++ b/api/map-token/function.json @@ -0,0 +1,19 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/api/requirements.txt b/api/requirements.txt index 287a5e36..9a69776b 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -3,4 +3,5 @@ # Manually managing azure-functions-worker may cause unexpected issues azure-functions==1.11.2 +azure-identity==1.15.0 requests==2.31.0 diff --git a/docker-compose.yml b/docker-compose.yml index 6aa68f1f..b4686b37 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: # environment variables set via GH Actions to test and build. - REACT_APP_API_ROOT - REACT_APP_IMAGE_API_ROOT - - REACT_APP_AZMAPS_KEY + - REACT_APP_AZMAPS_CLIENT_ID - REACT_APP_ONEDS_TENANT_KEY - REACT_APP_HUB_URL volumes: @@ -34,6 +34,7 @@ services: - "8000:8000" volumes: - ./api:/usr/src + - ~/.azure:/root/.azure networks: pcdc-network: command: func host start --script-root ./ --cors "*" --port 7071 diff --git a/docs/index.md b/docs/index.md index 5f4af4ec..82148904 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,7 +16,6 @@ Explorer The Hub Use VS Code Use GitHub Codespaces -Batch Jobs Using QGIS Changelog ``` diff --git a/docs/overview/batch.md b/docs/overview/batch.md deleted file mode 100644 index f47b9204..00000000 --- a/docs/overview/batch.md +++ /dev/null @@ -1,58 +0,0 @@ -# Running batch jobs - -The [Planetary Computer Hub][hub] is a great option for *interactive* data analysis. But JupyterHub isn't primarily designed for -asynchronous (or batch) workflows, where you submit some kind of job and let it run to completion. - -The Hub includes a [kbatch](https://kbatch.readthedocs.io/en/latest/) service, which lets you submit jobs to run on the same -computing infrastructure as your interactive workflows. See the [installation instructions](https://kbatch.readthedocs.io/en/latest/#install) -for kbatch to get started. - -## Configuration - -Once you have `kbatch` installed, you can configure the Hub URL and token to use. - -First, visit the [token generation page][token] to generate a token - -![JupyterHub Admin page kkto generate a token.](../concepts/images/hub-token.png) - -Next, use `kbatch configure` - -```{code-block} console -$ kbatch configure --kbatch-url=https://pccompute.westeurope.cloudapp.azure.com/compute/services/kbatch --token='' -``` - -## Submit a Job - -Use `kbatch submit` to submit a "job", which is just some commands to run. At a minimum, your job needs to include - -1. A name to identify your job -2. A container image, which defines the software environment the job will run in -3. A command to run - -```{code-block} console -❯ kbatch job submit ... -``` - -List your jobs with `kbatch job list`: - -![kbatch job list output showing a few jobs](images/kbatch-job-list.png) - -See the [kbatch examples gallery][gallery] for more. - -## Job runtime - -These batch jobs run in the same compute environment as the JuptyerHub. In particular, they include access to Dask Gateway, so you're able to start -and stop Dask clusters just as if you were running on the hub. - -```{note} -Your jobs will *not* have access to your JupyterHub home directory. You'll need to submit any [code files][code] along with your job. -``` - -## Limitations - -`kbatch` jobs are simply commands to be executed. It doesn't offer any fancier workflow orchestration features like alerting, automatically parallelization, artifact management, etc. - -[hub]: environment.md -[token]: https://pccompute.westeurope.cloudapp.azure.com/compute/hub/token -[gallery]: https://kbatch.readthedocs.io/en/latest/examples/index.html -[code]: https://kbatch.readthedocs.io/en/latest/user-guide.html#submitting-code-files \ No newline at end of file diff --git a/docs/overview/environment.md b/docs/overview/environment.md index a25018ba..795dcc51 100644 --- a/docs/overview/environment.md +++ b/docs/overview/environment.md @@ -26,8 +26,9 @@ Select *Stop My Server* to stop your server and release all of the resources you ![JupyterHub menu to stop the server](images/hub-home.png) -Note that we will automatically stop notebook servers that appear idle or are older that 24 hours. If you expect a job to take longer -than 24 hours, then see [running batch jobs](./batch) for a way to submit long-running jobs. +Note that we will automatically stop notebook servers that appear idle or are older that 24 hours. +The Planetary Computer Hub is primarily intended for interactive computation on datasets +from our catalog. ## Using JupyterLab diff --git a/package-lock.json b/package-lock.json index fa773b19..49b35a22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "pc-datacatalog", - "version": "2024.1.0", + "version": "2024.1.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1516,165 +1516,165 @@ } }, "@fluentui/date-time-utilities": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.5.5.tgz", - "integrity": "sha512-P/qfyMIF1aWPVaZvgAE0u166Rp1Rfpymv63/NKQT1o56cc5LzfWTzjD2Ey1GyA+tn6dCf7g1ZXTpKo5H+CuM4Q==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.5.16.tgz", + "integrity": "sha512-l+mLfJ2VhdHjBpELLLPDaWgT7GMLynm2aqR7SttbEb6Jh7hc/7ck1MWm93RTb3gYVHYai8SENqimNcvIxHt/zg==", "requires": { - "@fluentui/set-version": "^8.2.5", + "@fluentui/set-version": "^8.2.14", "tslib": "^2.1.0" } }, "@fluentui/dom-utilities": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.2.5.tgz", - "integrity": "sha512-VGCtAmPU/3uj/QV4Kx7gO/H2vNrhNSB346sE7xM+bBtxj+hf/owaGTvN6/tuZ8HXQu8tjTf8+ubQ3d7D7DUIjA==", + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.2.14.tgz", + "integrity": "sha512-+4DVm5sNfJh+l8fM+7ylpOkGNZkNr4X1z1uKQPzRJ1PRhlnvc6vLpWNNicGwpjTbgufSrVtGKXwP5sf++r81lg==", "requires": { - "@fluentui/set-version": "^8.2.5", + "@fluentui/set-version": "^8.2.14", "tslib": "^2.1.0" } }, "@fluentui/font-icons-mdl2": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.9.tgz", - "integrity": "sha512-u2a45ZE7GDLOSLpKPIDykVfbZs48oLT1m12JUdz4oGTkR9bR0+l8cnQL/+rYTu0KcJp2V5F9zLyTpJdlZnV36w==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.32.tgz", + "integrity": "sha512-PCZMijJlDQ5Zy8oNb80vUD6I4ORiR03qFgDT8o08mAGu+KzQO96q4jm0rzPRQuI9CO7pDD/6naOo8UVrmhZ2Aw==", "requires": { - "@fluentui/set-version": "^8.2.5", - "@fluentui/style-utilities": "^8.9.2", - "@fluentui/utilities": "^8.13.7", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", "tslib": "^2.1.0" } }, "@fluentui/foundation-legacy": { - "version": "8.2.29", - "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.2.29.tgz", - "integrity": "sha512-apO4PcZtU8N4zFW9fLE6aEb6JdlPyssI98MHSvCWKMiUqWSUhimvIgJ75CT3XmbKdI3bXu/miKjWSOMIHsZkiQ==", - "requires": { - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/style-utilities": "^8.9.2", - "@fluentui/utilities": "^8.13.7", + "version": "8.2.52", + "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.2.52.tgz", + "integrity": "sha512-tHCD0m58Zja7wN1FTsvj4Gaj0B22xOhRTpyDzyvxRfjFGYPpR2Jgx/y/KRB3JTOX5EfJHAVzInyWZBeN5IfsVA==", + "requires": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", "tslib": "^2.1.0" } }, "@fluentui/keyboard-key": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.5.tgz", - "integrity": "sha512-c+B+mdEgj0B6fhYIjznesGi8Al1rTpdFNudpNmFoVjlhCle5qj5RBtM4WaT8XygdzAVQq7oHSXom0vd32+zAZg==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.14.tgz", + "integrity": "sha512-XzZHcyFEM20H23h3i15UpkHi2AhRBriXPGAHq0Jm98TKFppXehedjjEFuUsh+CyU5JKBhDalWp8TAQ1ArpNzow==", "requires": { "tslib": "^2.1.0" } }, "@fluentui/merge-styles": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.5.6.tgz", - "integrity": "sha512-i9Wy+7V+lKfX+UWRTrrK+3xm4aa8jl9tK2/7Ku696yWJ5v3D6xjRcMevfxUZDrZ3xS4/GRFfWKPHkAjzz/BQoQ==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.5.15.tgz", + "integrity": "sha512-4CdKwo4k1Un2QLulpSVIz/KMgLNBMgin4NPyapmKDMVuO1OOxJUqfocubRGNO5x9mKgAMMYwBKGO9i0uxMMpJw==", "requires": { - "@fluentui/set-version": "^8.2.5", + "@fluentui/set-version": "^8.2.14", "tslib": "^2.1.0" } }, "@fluentui/react": { - "version": "8.106.1", - "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.106.1.tgz", - "integrity": "sha512-cLagkBBsknKOYFXBeWBwAm5VryPeJFVUIgLa0XlXcxmeOco/IhmWlhY5gEEQ9YoX1dFyNVcYXWxNFR55KUpZoA==", - "requires": { - "@fluentui/date-time-utilities": "^8.5.5", - "@fluentui/font-icons-mdl2": "^8.5.9", - "@fluentui/foundation-legacy": "^8.2.29", - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/react-focus": "^8.8.15", - "@fluentui/react-hooks": "^8.6.17", - "@fluentui/react-portal-compat-context": "^9.0.4", - "@fluentui/react-window-provider": "^2.2.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/style-utilities": "^8.9.2", - "@fluentui/theme": "^2.6.23", - "@fluentui/utilities": "^8.13.7", + "version": "8.115.6", + "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.115.6.tgz", + "integrity": "sha512-lao6u6AfA9uE+jWsmmRriCYXlQ9IU3W2jlapJiOJGyQvF9JGdVCyKDi2w4dIvsJyhA4ucfcKqg+9EgyrgbWcNg==", + "requires": { + "@fluentui/date-time-utilities": "^8.5.16", + "@fluentui/font-icons-mdl2": "^8.5.32", + "@fluentui/foundation-legacy": "^8.2.52", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/react-focus": "^8.8.40", + "@fluentui/react-hooks": "^8.6.36", + "@fluentui/react-portal-compat-context": "^9.0.11", + "@fluentui/react-window-provider": "^2.2.18", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/theme": "^2.6.41", + "@fluentui/utilities": "^8.13.24", "@microsoft/load-themed-styles": "^1.10.26", "tslib": "^2.1.0" } }, "@fluentui/react-focus": { - "version": "8.8.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.15.tgz", - "integrity": "sha512-TRYOSkQ6hQmZnfiM8k8Uw7bVAi69iguFc4V6lO90QHZ3fHQxlIHBHEmBzysYDORKMwQbtbllCtkm9ImlhsxEJw==", - "requires": { - "@fluentui/keyboard-key": "^0.4.5", - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/style-utilities": "^8.9.2", - "@fluentui/utilities": "^8.13.7", + "version": "8.8.40", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.40.tgz", + "integrity": "sha512-ha0CbLv5EIbjYCtQky6LVZObxOeMfhixrgrzfXm3Ta2eGs1NyZRDm1VeM6acOolWB/8QiN/CbdGckjALli8L2g==", + "requires": { + "@fluentui/keyboard-key": "^0.4.14", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", "tslib": "^2.1.0" } }, "@fluentui/react-hooks": { - "version": "8.6.17", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.6.17.tgz", - "integrity": "sha512-UdsK3YZ6Rx5fCNFfIcyJ2il8j5Ypb7OQY+0Qe2nmrn+/NKrtCeFVCIAs+i5MzjlL5wOsX27YXZwqby2DBUuSPg==", + "version": "8.6.36", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.6.36.tgz", + "integrity": "sha512-kI0Z4Q4xHUs4SOmmI5n5OH5fPckqMSCovTRpiuxzCO2TNzLmfC861+nqf4Ygw/ChqNm2gWNZZfUADfnNAEsq+Q==", "requires": { - "@fluentui/react-window-provider": "^2.2.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/utilities": "^8.13.7", + "@fluentui/react-window-provider": "^2.2.18", + "@fluentui/set-version": "^8.2.14", + "@fluentui/utilities": "^8.13.24", "tslib": "^2.1.0" } }, "@fluentui/react-portal-compat-context": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.4.tgz", - "integrity": "sha512-qw2lmkxZ2TmgC0pB2dvFyrzVffxBdpCx1BdWRaF+MRGUlTxRtqfybSx3Edsqa6NMewc3J0ThLMFdVFBQ5Yafqw==", + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.11.tgz", + "integrity": "sha512-ubvW/ej0O+Pago9GH3mPaxzUgsNnBoqvghNamWjyKvZIViyaXUG6+sgcAl721R+qGAFac+A20akI5qDJz/xtdg==", "requires": { - "tslib": "^2.1.0" + "@swc/helpers": "^0.5.1" } }, "@fluentui/react-window-provider": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.6.tgz", - "integrity": "sha512-bcQM5mdi4ugVb30GNtde8sP173F+l9p7uQfgK/I8O07EfKHUHZeY4wj5arD53s1cUIQI0kxWJ5RD7upZNRQeQA==", + "version": "2.2.18", + "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.18.tgz", + "integrity": "sha512-nBKqxd0P8NmIR0qzFvka1urE2LVbUm6cse1I1T7TcOVNYa5jDf5BrO06+JRZfwbn00IJqOnIVoP0qONqceypWQ==", "requires": { - "@fluentui/set-version": "^8.2.5", + "@fluentui/set-version": "^8.2.14", "tslib": "^2.1.0" } }, "@fluentui/set-version": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.5.tgz", - "integrity": "sha512-DwJq9wIXLc8WkeJ/lqYM4Sv+R0Ccb6cy3cY1Bqaa5POsroVKIfL6W+njvAMOj3LO3+DaXo2aDeiUnnw70M8xIw==", + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.14.tgz", + "integrity": "sha512-f/QWJnSeyfAjGAqq57yjMb6a5ejPlwfzdExPmzFBuEOuupi8hHbV8Yno12XJcTW4I0KXEQGw+PUaM1aOf/j7jw==", "requires": { "tslib": "^2.1.0" } }, "@fluentui/style-utilities": { - "version": "8.9.2", - "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.9.2.tgz", - "integrity": "sha512-cnP3p73+9RDE91Dy6aWHgkkBLbtv9Ct66YD6Hgr/tU3th3Z/CJU2TcJ4EN8RqiIBCiO4LU+W2jrFFZNnXTAxEw==", - "requires": { - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/theme": "^2.6.23", - "@fluentui/utilities": "^8.13.7", + "version": "8.10.3", + "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.10.3.tgz", + "integrity": "sha512-pyO9BGkwIxXaIMVT6ma98GIZAgTjGc0LZ5iUai9GLIrFLQWnIKnS//hgUx8qG4AecUeqZ26Wb0e+Ale9NyPQCQ==", + "requires": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/theme": "^2.6.41", + "@fluentui/utilities": "^8.13.24", "@microsoft/load-themed-styles": "^1.10.26", "tslib": "^2.1.0" } }, "@fluentui/theme": { - "version": "2.6.23", - "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.6.23.tgz", - "integrity": "sha512-xuX3jHsIrB/LbgVwmZwiXkmCT+EY8c7b97wV2SGDpVUSsFDSQMQPZgQ3eAfXNA1ZLbgYcxBycm5f+gDxjlKA8g==", + "version": "2.6.41", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.6.41.tgz", + "integrity": "sha512-h9RguEzqzJ0+59ys5Kkp7JtsjhDUxBLmQunu5rpHp5Mp788OtEjI/n1a9FIcOAL/priPSQwXN7RbuDpeP7+aSw==", "requires": { - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/utilities": "^8.13.7", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/utilities": "^8.13.24", "tslib": "^2.1.0" } }, "@fluentui/utilities": { - "version": "8.13.7", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.13.7.tgz", - "integrity": "sha512-whH09ttg7DGzANijSFXTF//xJNYjTrMB6TcgC7ge+5suI7VVwsh3NZc9lYN2mKKUveHEOjLgeunEhQ3neAvn5Q==", + "version": "8.13.24", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.13.24.tgz", + "integrity": "sha512-/jo6hWCzTGCx06l2baAMwsjjBZ/dyMouls53uNaQLUGUUhUwXh/DcDDXMqLRJB3MaH9zvgfvRw61iKmm2s9fIA==", "requires": { - "@fluentui/dom-utilities": "^2.2.5", - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", + "@fluentui/dom-utilities": "^2.2.14", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", "tslib": "^2.1.0" } }, @@ -2533,6 +2533,14 @@ "loader-utils": "^2.0.0" } }, + "@swc/helpers": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", + "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", + "requires": { + "tslib": "^2.4.0" + } + }, "@testing-library/dom": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.13.0.tgz", @@ -4024,12 +4032,13 @@ "integrity": "sha512-LVAaGp/wkkgYJcjmHsoKx4juT1aQvJyPcW09MLCjVTh3V2cc6PnyempiLMNH5iMdfIX/zdbjUx2KDjMLCTdPeA==" }, "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz", + "integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==", "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" }, "dependencies": { "form-data": { @@ -4041,6 +4050,11 @@ "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" } } }, diff --git a/package.json b/package.json index 1edaf46a..c36d2a3f 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "pc-datacatalog", - "version": "2024.1.2", + "version": "2024.1.3", "private": true, "proxy": "http://api:7071/", "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.9", "@craco/craco": "7.0.0-alpha.3", - "@fluentui/react": "^8.106.1", + "@fluentui/react": "^8.115.6", "@radiantearth/stac-fields": "1.0.0-beta.7", "@reduxjs/toolkit": "^1.8.2", "@testing-library/jest-dom": "^5.16.4", @@ -29,7 +29,7 @@ "@types/react-router-hash-link": "^2.4.5", "@types/swagger-ui-react": "^4.11.0", "@uifabric/icons": "^7.7.2", - "axios": "^0.27.2", + "axios": "^0.28.0", "azure-maps-control": "^2.2.0", "azure-maps-drawing-tools": "^1.0.0", "buffer": "^6.0.3", diff --git a/src/components/stac/SpatialExtent.js b/src/components/stac/SpatialExtent.js index 0f932377..f80a0f40 100644 --- a/src/components/stac/SpatialExtent.js +++ b/src/components/stac/SpatialExtent.js @@ -2,6 +2,8 @@ import React, { useEffect, useState, useRef } from "react"; import * as atlas from "azure-maps-control"; import "azure-maps-control/dist/atlas.min.css"; import LabeledValue from "../controls/LabeledValue"; +import { AZMAPS_CLIENT_ID } from "utils/constants"; +import { fetchMapToken } from "pages/Explore/components/Map/helpers"; const SpatialExtent = ({ extent }) => { const mapRef = useRef(); @@ -85,8 +87,9 @@ const SpatialExtent = ({ extent }) => { style: "grayscale_light", renderWorldCopies: true, // This setting may need adjustment for showing whole-world bounds authOptions: { - authType: "subscriptionKey", - subscriptionKey: process.env.REACT_APP_AZMAPS_KEY, + authType: atlas.AuthenticationType.anonymous, + clientId: AZMAPS_CLIENT_ID, + getToken: fetchMapToken, }, }); diff --git a/src/pages/Explore/components/Map/components/MapReadyIndicator/index.tsx b/src/pages/Explore/components/Map/components/MapReadyIndicator/index.tsx new file mode 100644 index 00000000..75d2150e --- /dev/null +++ b/src/pages/Explore/components/Map/components/MapReadyIndicator/index.tsx @@ -0,0 +1,19 @@ +import { Spinner, SpinnerSize } from "@fluentui/react"; + +interface MapReadyIndicatorProps { + isMapReady: boolean; +} + +const MapReadyIndicator: React.FC = ({ isMapReady }) => { + if (isMapReady) { + return null; + } + + return ( +
+ +
+ ); +}; + +export default MapReadyIndicator; diff --git a/src/pages/Explore/components/Map/helpers.ts b/src/pages/Explore/components/Map/helpers.ts index 86a01b42..1e64ccb5 100644 --- a/src/pages/Explore/components/Map/helpers.ts +++ b/src/pages/Explore/components/Map/helpers.ts @@ -1,4 +1,5 @@ import * as atlas from "azure-maps-control"; +import axios from "axios"; import { DATA_URL, REQUEST_ENTITY, X_REQUEST_ENTITY } from "utils/constants"; import { IStacItem } from "types/stac"; import { ILayerState } from "pages/Explore/types"; @@ -33,6 +34,43 @@ export const addEntityHeader = ( return { headers: {}, url: url }; }; +let cachedToken: string | null = null; +let tokenExpiry: number | null = null; + +export const fetchMapToken = async ( + resolve: (value: string) => void, + reject: (reason?: any) => void +): Promise => { + const nowInSeconds = Math.floor(Date.now() / 1000); // Current time in seconds since epoch + + // Check if we have a valid token in the cache + if (cachedToken !== null && tokenExpiry !== null && nowInSeconds < tokenExpiry) { + resolve(cachedToken); + return; + } + + // If no valid cached token, fetch a new one + try { + const resp = await axios.get<{ token: string; expires_on: number }>( + `${DATA_URL}/config/map/token` + ); + + if (resp.status === 200 && resp.data.token && resp.data.expires_on) { + cachedToken = resp.data.token; + + // Subtract a small buffer (e.g., 5 minutes in seconds) to ensure + // we refresh the token before it actually expires + tokenExpiry = resp.data.expires_on - 5 * 60; + + resolve(cachedToken); + } else { + reject(new Error("Failed to fetch map token")); + } + } catch (error) { + reject(error); + } +}; + export const makeLayerId = (id: string) => `${mosaicLayerPrefix}${id}`; export const makeDatasourceId = (mapLayerId: string) => `${mapLayerId}-ds`; export const makeLayerOutlineId = (mapLayerId: string) => `${mapLayerId}-outline`; diff --git a/src/pages/Explore/components/Map/index.tsx b/src/pages/Explore/components/Map/index.tsx index 921cb686..ba9b0122 100644 --- a/src/pages/Explore/components/Map/index.tsx +++ b/src/pages/Explore/components/Map/index.tsx @@ -28,8 +28,10 @@ import MapSettingsControl from "./components/MapSettingsControl"; import { DEFAULT_MAP_STYLE } from "pages/Explore/utils/constants"; import LegendControl from "./components/LegendControl"; import { MobileViewSidebarButton } from "../MobileViewInMap/ViewInMap.index"; -import { addEntityHeader } from "./helpers"; +import { addEntityHeader, fetchMapToken } from "./helpers"; import { PreviewMessage } from "./components/ItemPreview/PreviewMessage"; +import { AZMAPS_CLIENT_ID } from "utils/constants"; +import MapReadyIndicator from "./components/MapReadyIndicator"; const mapContainerId: string = "viewer-map"; @@ -57,8 +59,9 @@ const ExploreMap = () => { style: DEFAULT_MAP_STYLE, renderWorldCopies: true, authOptions: { - authType: atlas.AuthenticationType.subscriptionKey, - subscriptionKey: process.env.REACT_APP_AZMAPS_KEY, + authType: atlas.AuthenticationType.anonymous, + clientId: AZMAPS_CLIENT_ID, + getToken: fetchMapToken, }, transformRequest: addEntityHeader, }); @@ -122,7 +125,7 @@ const ExploreMap = () => { ); - const loadingIndicator = ( + const tileLoadingIndicator = ( { return (
- {mapHandlers.areTilesLoading && loadingIndicator} + {mapHandlers.areTilesLoading && tileLoadingIndicator} {showZoomMsg && zoomMsg} {showExtentMsg && extentMsg} + diff --git a/src/utils/constants.js b/src/utils/constants.js index e863a1bb..37be4cb3 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -21,6 +21,12 @@ export const IMAGE_URL = process.env.REACT_APP_IMAGE_API_ROOT || ""; export const HUB_URL = process.env.REACT_APP_HUB_URL || ""; export const AUTH_URL = process.env.REACT_APP_AUTH_URL || apiRoot; +export const AZMAPS_CLIENT_ID = process.env.REACT_APP_AZMAPS_CLIENT_ID; + +if (!AZMAPS_CLIENT_ID) { + console.warn("AZMAPS_CLIENT_ID must be set"); +} + export const X_REQUEST_ENTITY = "X-PC-Request-Entity"; export const QS_REQUEST_ENTITY = "request_entity"; export const REQUEST_ENTITY = "explorer"; diff --git a/src/utils/index.ts b/src/utils/index.ts index 8216f4da..72ee4f1c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -174,6 +174,11 @@ export const a11yPostProcessDom = (dom: Document) => { dom.querySelectorAll(".highlight pre").forEach(el => { el.setAttribute("tabindex", "0"); }); + + //

tags with role="heading" need an aria-level attribute + dom + .querySelectorAll("p[role=heading]") + .forEach(el => el.setAttribute("aria-level", "3")); }; export const scrollToHash = (