diff --git a/.eslintrc.js b/.eslintrc.js index 5b390797..1c8f2bb5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,7 +5,8 @@ module.exports = { '@typescript-eslint', ], rules: { - "@typescript-eslint/ban-ts-comment": "off" + "@typescript-eslint/ban-ts-comment": "off", + "eqeqeq": ["error", "smart"], }, extends: [ 'eslint:recommended', diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-dashboard-tests.yml similarity index 75% rename from .github/workflows/e2e-tests.yml rename to .github/workflows/e2e-dashboard-tests.yml index 4665f676..a0396976 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-dashboard-tests.yml @@ -1,10 +1,10 @@ -name: e2e-tests +name: e2e-dashboard-tests on: pull_request: branches: - main paths: - - '.github/workflows/e2e-tests.yml' + - '.github/workflows/e2e-dashboard-tests.yml' - '**.py' - '**.ts' - '**.tsx' @@ -47,4 +47,10 @@ jobs: run: playwright install - name: Run e2e tests - run: pytest e2e_tests + run: | + if [ "${{ matrix.optuna-version }}" = "optuna==2.10.0" ]; then + ignore_option="--ignore e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py" + else + ignore_option="" + fi + pytest e2e_tests/test_dashboard $ignore_option diff --git a/.github/workflows/e2e-standalone-tests.yml b/.github/workflows/e2e-standalone-tests.yml new file mode 100644 index 00000000..2de6d610 --- /dev/null +++ b/.github/workflows/e2e-standalone-tests.yml @@ -0,0 +1,51 @@ +name: e2e-standalone-tests +on: + pull_request: + branches: + - main + paths: + - '.github/workflows/e2e-standalone-tests.yml' + - '**.py' + - '**.ts' + - '**.tsx' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' +jobs: + test: + runs-on: ubuntu-20.04 + strategy: + matrix: + optuna-version: ['optuna==2.10.0', 'git+https://github.com/optuna/optuna.git'] + steps: + - uses: actions/checkout@v3 + + - name: Install Rust toolchains + uses: dtolnay/rust-toolchain@stable + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: '18' + + - name: Setup Optuna ${{ matrix.optuna-version }} + run: | + python -m pip install --progress-bar off --upgrade pip setuptools + python -m pip install --progress-bar off --upgrade ${{ matrix.optuna-version }} + + - name: Install dependencies + run: | + python -m pip install --progress-bar off . + python -m pip install --progress-bar off pytest-playwright + + - name: Build standalone_app + run: make MODE="prd" standalone_app/public/bundle.js + + - name: Install the required browsers + run: playwright install + + - name: Run e2e tests + run: pytest e2e_tests/test_standalone diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 8fee4e44..60dd6ea0 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -6,6 +6,7 @@ on: paths: - '.github/workflows/python-tests.yml' - '**.py' + - 'pyproject.toml' jobs: lint: runs-on: ubuntu-latest @@ -57,7 +58,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: '3.11' architecture: x64 - name: Install dependencies run: | diff --git a/.gitignore b/.gitignore index 49c137e0..646c1439 100644 --- a/.gitignore +++ b/.gitignore @@ -39,5 +39,4 @@ coverage.xml .vscode/ .DS_Store tmp/ -examples/preferential-optimization/artifact/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04002ad7..df519b53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,6 +83,7 @@ $ pytest python_tests/ ``` $ pip install -r requirements.txt +$ playwright install $ pytest e2e_tests ``` @@ -92,6 +93,9 @@ If you want to create a screenshot for each test, please run a following command $ pytest e2e_tests --screenshot on --output tmp ``` +If you want to generate a locator in each webpage, please use the playwright codegen. See [this page](https://playwright.dev/python/docs/codegen-intro) for more details. + + For more detail options, you can check [this page](https://playwright.dev/python/docs/test-runners). #### Linters (flake8, black and mypy) diff --git a/README.md b/README.md index 62edd6c8..13022125 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square) [![PyPI - Downloads](https://img.shields.io/pypi/dm/optuna-dashboard)](https://pypistats.org/packages/optuna-dashboard) [![Read the Docs](https://readthedocs.org/projects/optuna-dashboard/badge/?version=latest)](https://optuna-dashboard.readthedocs.io/en/latest/?badge=latest) +[![Codecov](https://codecov.io/gh/optuna/optuna-dashboard/branch/main/graph/badge.svg)](https://codecov.io/gh/optuna/optuna-dashboard) Real-time dashboard for [Optuna](https://github.com/optuna/optuna). @@ -77,6 +78,18 @@ $ docker run -it --rm -p 8080:8080 ghcr.io/optuna/optuna-dashboard postgresql+ps +## Jupyter Lab Extension (Experimental) + +You can install the Jupyter Lab extension via [PyPI](https://pypi.org/project/jupyterlab-optuna/). + +``` +$ pip install jupyterlab jupyterlab-optuna +``` + +Jupyter Lab Extension + +To use, click the tile to launch the extension, and enter your Optuna’s storage URL (e.g. `sqlite:///db.sqlite3`) in the dialog. + ## Browser-only version (Experimental) Browser-only version @@ -93,7 +106,7 @@ https://optuna.github.io/optuna-dashboard/ You can install the VS Code extension via [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=Optuna.optuna-dashboard#overview). -VSCode Extension +VS Code Extension Please right-click the SQLite3 files (`*.db` or `*.sqlite3`) in the VS Code file explorer and select the "Open in Optuna Dashboard" command from the dropdown menu. This extension leverages the browser-only version of Optuna Dashboard, so the same limitations apply. diff --git a/docs/_static/jupyterlab-extension.png b/docs/_static/jupyterlab-extension.png new file mode 100644 index 00000000..7652e87f Binary files /dev/null and b/docs/_static/jupyterlab-extension.png differ diff --git a/docs/api.rst b/docs/api.rst index 18f8bc72..e26dcfd6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -44,6 +44,7 @@ Preferential Optimization optuna_dashboard.preferential.create_study optuna_dashboard.preferential.load_study optuna_dashboard.preferential.PreferentialStudy + optuna_dashboard.preferential.samplers.gp.PreferentialGPSampler optuna_dashboard.register_preference_feedback_component Streamlit diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 295dd55c..d3b040c6 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -178,6 +178,19 @@ or $ pip install uwsgi $ uwsgi --http :8080 --workeers 4 --wsgi-file wsgi.py +Jupyter Lab Extension (Experimental) +-------------------------------- + +You can install the Jupyter Lab extension via `PyPI `_. + +.. figure:: _static/jupyterlab-extension.png + :alt: Screenshot for the Jupyter Lab Extension + :align: center + :width: 800px + + Jupyter Lab Extension + +To use, click the tile to launch the extension, and enter your Optuna’s storage URL (e.g. ``sqlite:///db.sqlite3``) in the dialog. Browser-only version (Experimental) ----------------------------------- diff --git a/docs/tutorials/hitl.rst b/docs/tutorials/hitl.rst index e06709c8..ad4f5e7d 100644 --- a/docs/tutorials/hitl.rst +++ b/docs/tutorials/hitl.rst @@ -95,7 +95,7 @@ Given the above system, we carry out HITL optimization as follows: Environment setup ^^^^^^^^^^^^^^^^^ -To run `the script `_ used in this tutorial, you need to install following libraries: +To run `the script `_ used in this tutorial, you need to install following libraries: .. code-block:: console @@ -109,7 +109,7 @@ You will use SQLite for the storage backend in this tutorial. Ensure that the fo Execution of the HITL optimization script ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Run a python script below which you copied from `main.py `_ +Run a python script below which you copied from `main.py `_ .. code-block:: console @@ -150,7 +150,7 @@ Click the third item in the sidebar. You will see a list of all trials. .. image:: ./images/hitl10.png -For each trial, you can see its details such as RGB parameter values and importantly, the generated image based on these values. +For each trial, you can see its details such as RGB parameter values and importantly, the generated image based on these values. .. image:: ./images/hitl11.gif :width: 90% @@ -189,21 +189,21 @@ Let’s walk through the script we used for the optimization. r = trial.suggest_int("r", 0, 255) g = trial.suggest_int("g", 0, 255) b = trial.suggest_int("b", 0, 255) - + # 2. Generate image image_path = f"tmp/sample-{trial.number}.png" image = Image.new("RGB", (320, 240), color=(r, g, b)) image.save(image_path) - + # 3. Upload Artifact artifact_id = upload_artifact(trial, image_path, artifact_store) artifact_path = get_artifact_path(trial, artifact_id) - + # 4. Save Note note = textwrap.dedent( f"""\ ## Trial {trial.number} - + ![generated-image]({artifact_path}) """ ) @@ -222,10 +222,10 @@ In the ``suggest_and_generate_image`` function, a new Trial is obtained and new sampler=optuna.samplers.TPESampler(constant_liar=True, n_startup_trials=5), load_if_exists=True, ) - + # 2. Set an objective name study.set_metric_names(["Looks like sunset color?"]) - + # 3. Register ChoiceWidget register_objective_form_widgets( study, @@ -237,7 +237,7 @@ In the ``suggest_and_generate_image`` function, a new Trial is obtained and new ), ], ) - + # 4. Start Human-in-the-loop Optimization n_batch = 4 while True: @@ -259,17 +259,17 @@ The function ``start_optimization`` defines our loop for HITL optimization to ge def main() -> NoReturn: tmp_path = os.path.join(os.path.dirname(__file__), "tmp") - + # 1. Create Artifact Store artifact_path = os.path.join(os.path.dirname(__file__), "artifact") artifact_store = FileSystemArtifactStore(artifact_path) - + if not os.path.exists(artifact_path): os.mkdir(artifact_path) - + if not os.path.exists(tmp_path): os.mkdir(tmp_path) - + # 2. Run optimize loop start_optimization(artifact_store) diff --git a/docs/tutorials/preferential-optimization.rst b/docs/tutorials/preferential-optimization.rst index 4d2644ea..ab7ec654 100644 --- a/docs/tutorials/preferential-optimization.rst +++ b/docs/tutorials/preferential-optimization.rst @@ -5,11 +5,11 @@ What is Preferential Optimization? ---------------------------------- Preferential optimization is a method for optimizing hyperparameters, focusing of human preferences, by determining which trial is superior when comparing a pair. -It differs from `human-in-the-loop optimization utilizing objective form widgets `_, +It differs from :ref:`human-in-the-loop optimization utilizing objective form widgets `, which relies on absolute evaluations, as it significantly reduces fluctuations in evaluators' criteria, thus ensuring more consistent results. In this tutorial, we'll interactively optimize RGB values to generate a color resembling a "sunset hue", -aligining with the problem setting in `this tutorial `_. +aligining with the problem setting in :ref:`this tutorial `. Familiarity with the tutorial ob objective form widgets may enhance your understanding. How to Run Preferential Optimization @@ -27,7 +27,7 @@ First, ensure the necessary packages are installed by executing the following co .. code-block:: console - $ pip install "optuna>=3.3.0" "optuna-dashboard>=0.13.0b1" pillow botorch + $ pip install "optuna>=3.3.0" "optuna-dashboard[preferential]>=0.13.0b1" pillow Next, execute the Python script, copied from `generator.py`_. @@ -99,7 +99,7 @@ enabling the Optuna Dashboard to display images on the evaluation feedback page. register_preference_feedback_component(study, "artifact", user_attr_key) Following this, we create a loop that continuously checks if new trials should be generated, awaiting human evaluation if not. -Within the while loop, new trials are generated if the condition :meth:`~optuna_dashboard.preferential.PreferentialStudy.should_generate` returns ``True``. +Within the while loop, new trials are generated if the condition :meth:`~optuna_dashboard.preferential.PreferentialStudy.should_generate` returns ``True``. For each trial, RGB values are sampled, an image is generated with these values, saved temporarily. Then the image is uploaded to the artifact store, and finally, the ``artifact_id`` is stored to the key, which is specified via :func:`~optuna_dashboard.register_preference_feedback_component`. @@ -127,4 +127,4 @@ Then the image is uploaded to the artifact store, and finally, the ``artifact_id artifact_id = upload_artifact(trial, image_path, artifact_store) trial.set_user_attr(user_attr_key, artifact_id) -.. _generator.py: https://github.com/optuna/optuna-dashboard/blob/main/examples/preferential-optimization/generator.py +.. _generator.py: https://github.com/optuna/optuna-examples/blob/main/dashboard/preferential-optimization/generator.py diff --git a/e2e_tests/test_usecases/__init__.py b/e2e_tests/test_dashboard/__init__.py similarity index 100% rename from e2e_tests/test_usecases/__init__.py rename to e2e_tests/test_dashboard/__init__.py diff --git a/e2e_tests/test_dashboard/test_usecases/__init__.py b/e2e_tests/test_dashboard/test_usecases/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py b/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py new file mode 100644 index 00000000..0b19fea0 --- /dev/null +++ b/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py @@ -0,0 +1,129 @@ +import re + +import optuna +from optuna.trial import TrialState +from optuna_dashboard import ChoiceWidget +from optuna_dashboard import register_objective_form_widgets +from playwright.sync_api import expect +from playwright.sync_api import Page +import pytest + +from ...test_server import make_test_server +from ...utils import clear_inmemory_cache + + +def make_test_storage() -> optuna.storages.InMemoryStorage: + storage = optuna.storages.InMemoryStorage() + sampler = optuna.samplers.RandomSampler(seed=0) + + study = optuna.create_study( + study_name="preferential_optimization", + storage=storage, + sampler=sampler, + ) + + register_objective_form_widgets( + study, + widgets=[ + ChoiceWidget( + choices=["Good", "So-so", "Bad"], + values=[-1, 0, 1], + ), + ], + ) + + n_batch = 4 + while True: + running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,)) + if len(running_trials) >= n_batch: + break + study.ask() + + return storage + + +@pytest.fixture +def storage() -> optuna.storages.InMemoryStorage: + clear_inmemory_cache() + storage = make_test_storage() + return storage + + +@pytest.fixture +def server_url(request: pytest.FixtureRequest, storage: optuna.storages.InMemoryStorage) -> str: + return make_test_server(request, storage) + + +def test_preferential_optimization( + page: Page, + storage: optuna.storages.InMemoryStorage, + server_url: str, +) -> None: + summaries = optuna.get_all_study_summaries(storage) + study_id = summaries[0]._study_id + url = f"{server_url}/studies/{study_id}/trials" + + page.goto(url) + + # Confirm that the trial list page is displayed. + expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text( + "Trial 0 (trial_id=0)" + ) + page.get_by_label("Filter").click() + # Confirm that all trials are running. + expect( + page.get_by_text("Complete (0)Pruned (0)Fail (0)Running (4)Waiting (0)") + ).to_be_visible() + page.locator(".MuiBackdrop-root").click() + + # Confirm that the trial detail page is displayed. + expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text( + "Trial 0 (trial_id=0)" + ) + # This trial is running. + expect(page.get_by_text("Running", exact=True).nth(4)).to_be_visible() + page.get_by_label("Bad").check() + page.get_by_role("button", name="Submit").click() + # This trial is completed and is the best trial. + expect(page.get_by_text("Complete").nth(1)).to_be_visible() + expect(page.get_by_text("Best Trial").nth(1)).to_be_visible() + + # Move the next trial page. + page.get_by_role("button", name="Trial 1 Running").click() + # Confirm that the trial detail page is displayed. + expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text( + "Trial 1 (trial_id=1)" + ) + # This trial is running. + expect(page.get_by_text("Running", exact=True).nth(3)).to_be_visible() + page.get_by_label("So-so").check() + page.get_by_role("button", name="Submit").click() + # This trial is completed and is the best trial. + expect(page.get_by_text("Complete").nth(2)).to_be_visible() + expect(page.get_by_text("Best Trial").nth(1)).to_be_visible() + + # Move the next trial page. + page.get_by_role("button", name="Trial 2 Running").click() + # Confirm that the trial detail page is displayed. + expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text( + "Trial 2 (trial_id=2)" + ) + # This trial is running. + expect(page.get_by_text("Running", exact=True).nth(2)).to_be_visible() + page.get_by_label("Good").check() + page.get_by_role("button", name="Submit").click() + # This trial is completed and is the best trial. + expect(page.get_by_text("Complete").nth(3)).to_be_visible() + expect(page.get_by_text("Best Trial").nth(1)).to_be_visible() + + # Move the next trial page. + page.get_by_role("button", name="Trial 3 Running").click() + # Confirm that the trial detail page is displayed. + expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text( + "Trial 3 (trial_id=3)" + ) + # This trial is running. + expect(page.get_by_text("Running", exact=True).nth(1)).to_be_visible() + page.get_by_role("button", name="Fail Trial").click() + # This trial is failed. + expect(page.get_by_text("Fail").nth(1)).to_be_visible() diff --git a/e2e_tests/test_usecases/test_study_history.py b/e2e_tests/test_dashboard/test_usecases/test_study_history.py similarity index 94% rename from e2e_tests/test_usecases/test_study_history.py rename to e2e_tests/test_dashboard/test_usecases/test_study_history.py index e37b8f5a..f2d937bb 100644 --- a/e2e_tests/test_usecases/test_study_history.py +++ b/e2e_tests/test_dashboard/test_usecases/test_study_history.py @@ -2,7 +2,8 @@ import optuna from playwright.sync_api import Page import pytest -from ..test_server import make_test_server +from ...test_server import make_test_server +from ...utils import clear_inmemory_cache def make_test_storage() -> optuna.storages.InMemoryStorage: @@ -23,6 +24,7 @@ def make_test_storage() -> optuna.storages.InMemoryStorage: @pytest.fixture def storage() -> optuna.storages.InMemoryStorage: + clear_inmemory_cache() storage = make_test_storage() return storage diff --git a/e2e_tests/visual_regression_test.py b/e2e_tests/test_dashboard/visual_regression_test.py similarity index 99% rename from e2e_tests/visual_regression_test.py rename to e2e_tests/test_dashboard/visual_regression_test.py index d83fbb5f..ab189343 100644 --- a/e2e_tests/visual_regression_test.py +++ b/e2e_tests/test_dashboard/visual_regression_test.py @@ -4,11 +4,13 @@ import optuna from playwright.sync_api import Page import pytest -from .test_server import make_test_server +from ..test_server import make_test_server +from ..utils import clear_inmemory_cache @pytest.fixture def storage() -> optuna.storages.InMemoryStorage: + clear_inmemory_cache() storage = optuna.storages.InMemoryStorage() return storage diff --git a/e2e_tests/test_server.py b/e2e_tests/test_server.py index b6d14734..a15acc68 100644 --- a/e2e_tests/test_server.py +++ b/e2e_tests/test_server.py @@ -1,4 +1,6 @@ +import http.server import socket +import socketserver import threading from wsgiref.simple_server import make_server @@ -33,3 +35,29 @@ def make_test_server( request.addfinalizer(stop_server) return f"http://{addr}:{port}/dashboard" + + +def make_standalone_server(request: pytest.FixtureRequest) -> str: + addr = "127.0.0.1" + port = get_free_port() + directory = "./standalone_app/" + + Handler = http.server.SimpleHTTPRequestHandler + httpd = socketserver.TCPServer( + ("", port), lambda *args, **kwargs: Handler(*args, directory=directory, **kwargs) + ) + + def serve_httpd(): + httpd.serve_forever() + + thread = threading.Thread(target=serve_httpd) + thread.start() + + def stop_server() -> None: + httpd.shutdown() + httpd.server_close() + thread.join() + + request.addfinalizer(stop_server) + + return f"http://{addr}:{port}" diff --git a/e2e_tests/test_standalone/__init__.py b/e2e_tests/test_standalone/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/e2e_tests/test_standalone/test_study_list.py b/e2e_tests/test_standalone/test_study_list.py new file mode 100644 index 00000000..b0d4735e --- /dev/null +++ b/e2e_tests/test_standalone/test_study_list.py @@ -0,0 +1,22 @@ +from playwright.sync_api import Page +import pytest + +from ..test_server import make_standalone_server + + +@pytest.fixture +def server_url(request: pytest.FixtureRequest) -> str: + return make_standalone_server(request) + + +def test_home( + page: Page, + server_url: str, +) -> None: + url = f"{server_url}" + page.goto(url) + element = page.get_by_role("heading") + assert element is not None + title = element.text_content() + assert title is not None + assert title == "Optuna Dashboard (Wasm ver.)" diff --git a/e2e_tests/utils.py b/e2e_tests/utils.py new file mode 100644 index 00000000..68952ae9 --- /dev/null +++ b/e2e_tests/utils.py @@ -0,0 +1,9 @@ +from optuna_dashboard._storage import trials_cache +from optuna_dashboard._storage import trials_cache_lock +from optuna_dashboard._storage import trials_last_fetched_at + + +def clear_inmemory_cache() -> None: + with trials_cache_lock: + trials_cache.clear() + trials_last_fetched_at.clear() diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..ab86ed0a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,5 @@ +Optuna Dashboard Examples +========================= + +Example files have been moved to the [optuna/optuna-examples](https://github.com/optuna/optuna-examples/) repoistory. +You can find the dashboard-related examples in the [dashboard](https://github.com/optuna/optuna-examples/tree/main/dashboard) directory. diff --git a/examples/hitl/main.py b/examples/hitl/main.py deleted file mode 100644 index 7844bc7a..00000000 --- a/examples/hitl/main.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import textwrap -import time -from typing import NoReturn - -import optuna -from optuna.artifacts import FileSystemArtifactStore -from optuna.artifacts import upload_artifact -from optuna.trial import TrialState -from optuna_dashboard import ChoiceWidget -from optuna_dashboard import register_objective_form_widgets -from optuna_dashboard import save_note -from optuna_dashboard.artifact import get_artifact_path -from PIL import Image - - -def suggest_and_generate_image( - study: optuna.Study, artifact_store: FileSystemArtifactStore -) -> None: - # 1. Ask new parameters - trial = study.ask() - r = trial.suggest_int("r", 0, 255) - g = trial.suggest_int("g", 0, 255) - b = trial.suggest_int("b", 0, 255) - - # 2. Generate image - image_path = f"tmp/sample-{trial.number}.png" - image = Image.new("RGB", (320, 240), color=(r, g, b)) - image.save(image_path) - - # 3. Upload Artifact - artifact_id = upload_artifact(trial, image_path, artifact_store) - artifact_path = get_artifact_path(trial, artifact_id) - - # 4. Save Note - note = textwrap.dedent( - f"""\ - ## Trial {trial.number} - - ![generated-image]({artifact_path}) - """ - ) - save_note(trial, note) - - -def start_optimization(artifact_store: FileSystemArtifactStore) -> NoReturn: - # 1. Create Study - study = optuna.create_study( - study_name="Human-in-the-loop Optimization", - storage="sqlite:///db.sqlite3", - sampler=optuna.samplers.TPESampler(constant_liar=True, n_startup_trials=5), - load_if_exists=True, - ) - - # 2. Set an objective name - study.set_metric_names(["Looks like sunset color?"]) - - # 3. Register ChoiceWidget - register_objective_form_widgets( - study, - widgets=[ - ChoiceWidget( - choices=["Good 👍", "So-so👌", "Bad 👎"], - values=[-1, 0, 1], - description="Please input your score!", - ), - ], - ) - - # 4. Start Human-in-the-loop Optimization - n_batch = 4 - while True: - running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,)) - if len(running_trials) >= n_batch: - time.sleep(1) # Avoid busy-loop - continue - suggest_and_generate_image(study, artifact_store) - - -def main() -> NoReturn: - tmp_path = os.path.join(os.path.dirname(__file__), "tmp") - - # 1. Create Artifact Store - artifact_path = os.path.join(os.path.dirname(__file__), "artifact") - artifact_store = FileSystemArtifactStore(artifact_path) - - if not os.path.exists(artifact_path): - os.mkdir(artifact_path) - - if not os.path.exists(tmp_path): - os.mkdir(tmp_path) - - # 2. Run optimize loop - start_optimization(artifact_store) - - -if __name__ == "__main__": - main() diff --git a/examples/preferential-optimization/evaluator.sh b/examples/preferential-optimization/evaluator.sh deleted file mode 100755 index f7ebeca5..00000000 --- a/examples/preferential-optimization/evaluator.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -optuna-dashboard sqlite:///example.db --artifact-dir ./artifact \ No newline at end of file diff --git a/examples/preferential-optimization/generator.py b/examples/preferential-optimization/generator.py deleted file mode 100644 index a26343bb..00000000 --- a/examples/preferential-optimization/generator.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -import os -import tempfile -import time -from typing import NoReturn - -from optuna.artifacts import FileSystemArtifactStore -from optuna.artifacts import upload_artifact -from optuna_dashboard import register_preference_feedback_component -from optuna_dashboard.preferential import create_study -from optuna_dashboard.preferential.samplers.gp import PreferentialGPSampler -from PIL import Image - - -STORAGE_URL = "sqlite:///example.db" -artifact_path = os.path.join(os.path.dirname(__file__), "artifact") -artifact_store = FileSystemArtifactStore(base_path=artifact_path) -os.makedirs(artifact_path, exist_ok=True) - - -def main() -> NoReturn: - study = create_study( - n_generate=4, - study_name="Preferential Optimization", - storage=STORAGE_URL, - sampler=PreferentialGPSampler(), - load_if_exists=True, - ) - # Change the component, displayed on the human feedback pages. - # By default (component_type="note"), the Trial's Markdown note is displayed. - user_attr_key = "rgb_image" - register_preference_feedback_component(study, "artifact", user_attr_key) - - with tempfile.TemporaryDirectory() as tmpdir: - while True: - # If study.should_generate() returns False, - # the generator waits for human evaluation. - if not study.should_generate(): - time.sleep(0.1) # Avoid busy-loop - continue - - trial = study.ask() - # 1. Ask new parameters - r = trial.suggest_int("r", 0, 255) - g = trial.suggest_int("g", 0, 255) - b = trial.suggest_int("b", 0, 255) - - # 2. Generate image - image_path = os.path.join(tmpdir, f"sample-{trial.number}.png") - image = Image.new("RGB", (320, 240), color=(r, g, b)) - image.save(image_path) - - # 3. Upload Artifact and set artifact_id to trial.user_attrs["rgb_image"]. - artifact_id = upload_artifact(trial, image_path, artifact_store) - trial.set_user_attr(user_attr_key, artifact_id) - - -if __name__ == "__main__": - main() diff --git a/examples/streamlit_plugin/rgb_evaluator.py b/examples/streamlit_plugin/rgb_evaluator.py deleted file mode 100644 index 5836dbd6..00000000 --- a/examples/streamlit_plugin/rgb_evaluator.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import annotations - -import os -import shutil -import tempfile -import uuid - -import optuna -from optuna.trial import TrialState -from optuna_dashboard.artifact.file_system import FileSystemBackend -from optuna_dashboard.streamlit import render_objective_form_widgets -from optuna_dashboard.streamlit import render_trial_note - -import streamlit as st - - -artifact_path = os.path.join(os.path.dirname(__file__), "artifact") -artifact_backend = FileSystemBackend(base_path=artifact_path) - - -def get_tmp_dir() -> str: - if "tmp_dir" not in st.session_state: - tmp_dir_name = str(uuid.uuid4()) - tmp_dir_path = os.path.join(tempfile.gettempdir(), tmp_dir_name) - os.makedirs(tmp_dir_path, exist_ok=True) - st.session_state.tmp_dir = tmp_dir_path - - return st.session_state.tmp_dir - - -def start_streamlit() -> None: - tmpdir = get_tmp_dir() - study = optuna.load_study( - storage="sqlite:///streamlit-db.sqlite3", study_name="Human-in-the-loop Optimization" - ) - selected_trial = st.sidebar.selectbox("Trial", study.trials, format_func=lambda t: t.number) - - if selected_trial is None: - return - render_trial_note(study, selected_trial) - artifact_id = selected_trial.user_attrs.get("artifact_id") - if artifact_id: - with artifact_backend.open(artifact_id) as fsrc: - tmp_img_path = os.path.join(tmpdir, artifact_id + ".png") - with open(tmp_img_path, "wb") as fdst: - shutil.copyfileobj(fsrc, fdst) - st.image(tmp_img_path, caption="Image") - - if selected_trial.state == TrialState.RUNNING: - render_objective_form_widgets(study, selected_trial) - - -if __name__ == "__main__": - start_streamlit() diff --git a/examples/streamlit_plugin/rgb_generator.py b/examples/streamlit_plugin/rgb_generator.py deleted file mode 100644 index fc76ad02..00000000 --- a/examples/streamlit_plugin/rgb_generator.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import annotations - -import os -import tempfile -import time -from typing import NoReturn - -import optuna -from optuna.trial import TrialState -from optuna_dashboard import ChoiceWidget -from optuna_dashboard import register_objective_form_widgets -from optuna_dashboard import save_note -from optuna_dashboard.artifact import upload_artifact -from optuna_dashboard.artifact.file_system import FileSystemBackend -from PIL import Image - - -def suggest_and_generate_image( - study: optuna.Study, artifact_backend: FileSystemBackend, tmpdir: str -) -> None: - # 1. Ask new parameters - trial = study.ask() - r = trial.suggest_int("r", 0, 255) - g = trial.suggest_int("g", 0, 255) - b = trial.suggest_int("b", 0, 255) - - # 2. Generate image - image_path = os.path.join(tmpdir, f"sample-{trial.number}.png") - image = Image.new("RGB", (320, 240), color=(r, g, b)) - image.save(image_path) - - # 3. Upload Artifact - artifact_id = upload_artifact(artifact_backend, trial, image_path) - trial.set_user_attr("artifact_id", artifact_id) - - # 4. Save Note - save_note(trial, f"## Trial {trial.number}") - - -def main() -> NoReturn: - # 1. Create Artifact Store - artifact_path = os.path.join(os.path.dirname(__file__), "artifact") - artifact_backend = FileSystemBackend(base_path=artifact_path) - - if not os.path.exists(artifact_path): - os.mkdir(artifact_path) - - # 2. Create Study - study = optuna.create_study( - study_name="Human-in-the-loop Optimization", - storage="sqlite:///streamlit-db.sqlite3", - sampler=optuna.samplers.TPESampler(constant_liar=True, n_startup_trials=5), - load_if_exists=True, - ) - study.set_metric_names(["Looks like sunset color?"]) - - # 4. Register ChoiceWidget - register_objective_form_widgets( - study, - widgets=[ - ChoiceWidget( - choices=["Good 👍", "So-so👌", "Bad 👎"], - values=[-1, 0, 1], - description="Please input your score!", - ), - ], - ) - - # 5. Start Human-in-the-loop Optimization - n_batch = 4 - with tempfile.TemporaryDirectory() as tmpdir: - while True: - running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,)) - if len(running_trials) >= n_batch: - time.sleep(1) # Avoid busy-loop - continue - suggest_and_generate_image(study, artifact_backend, tmpdir) - - -if __name__ == "__main__": - main() diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py index ea2a8dbe..2ca1d487 100644 --- a/optuna_dashboard/__init__.py +++ b/optuna_dashboard/__init__.py @@ -17,4 +17,4 @@ from ._note import save_note # noqa from ._preference_setting import register_preference_feedback_component # noqa -__version__ = "0.13.0b1" +__version__ = "0.14.0" diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 812201ad..ead27188 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -1,8 +1,12 @@ from __future__ import annotations +import csv import functools +import io +from itertools import chain import logging import os +import re import typing from typing import Any from typing import Optional @@ -152,6 +156,7 @@ def create_app( storage=storage, study_name=dst_study_name, directions=src_study.directions ) dst_study.add_trials(src_study.get_trials(deepcopy=False)) + note.copy_notes(storage, src_study, dst_study) except DuplicatedStudyError: response.status = 400 # Bad request return {"reason": f"study_name={dst_study_name} is duplicaated"} @@ -448,6 +453,48 @@ def create_app( response.status = 204 # No content return {} + @app.get("/csv/") + def download_csv(study_id: int) -> BottleViewReturn: + # Create a CSV file + try: + study_name = storage.get_study_name_from_id(study_id) + study = optuna.load_study(storage=storage, study_name=study_name) + except KeyError: + response.status = 404 # Not found + return {"reason": f"study_id={study_id} is not found"} + trials = study.trials + param_names = sorted(set(chain.from_iterable([t.params.keys() for t in trials]))) + user_attr_names = sorted(set(chain.from_iterable([t.user_attrs.keys() for t in trials]))) + param_names_header = [f"Param {x}" for x in param_names] + user_attr_names_header = [f"UserAttribute {x}" for x in user_attr_names] + n_objs = len(study.directions) + if study.metric_names is not None: + value_header = study.metric_names + else: + value_header = ["Value"] if n_objs == 1 else [f"Objective {x}" for x in range(n_objs)] + column_names = ( + ["Number", "State"] + value_header + param_names_header + user_attr_names_header + ) + + buf = io.StringIO("") + writer = csv.writer(buf) + writer.writerow(column_names) + for frozen_trial in trials: + row = [frozen_trial.number, frozen_trial.state.name] + row.extend(frozen_trial.values if frozen_trial.values is not None else [None] * n_objs) + row.extend([frozen_trial.params.get(name, None) for name in param_names]) + row.extend([frozen_trial.user_attrs.get(name, None) for name in user_attr_names]) + writer.writerow(row) + + # Set response headers + output_name = "-".join(re.sub(r'[\\/:*?"<>|]+', "", study_name).split(" ")) + response.headers["Content-Type"] = "text/csv; chatset=cp932" + response.headers["Content-Disposition"] = f"attachment; filename={output_name}.csv" + + # Response body + buf.seek(0) + return buf.read() + @app.get("/favicon.ico") def favicon() -> BottleViewReturn: use_gzip = "gzip" in request.headers["Accept-Encoding"] diff --git a/optuna_dashboard/_cached_extra_study_property.py b/optuna_dashboard/_cached_extra_study_property.py index 2f27fa64..24e22372 100644 --- a/optuna_dashboard/_cached_extra_study_property.py +++ b/optuna_dashboard/_cached_extra_study_property.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import numbers import threading from typing import List from typing import Optional @@ -85,9 +86,8 @@ class _CachedExtraStudyProperty: self._cursor = next_cursor def _update_user_attrs(self, trial: FrozenTrial) -> None: - # TODO(c-bata): Support numpy-specific number types. current_user_attrs = { - k: not isinstance(v, bool) and isinstance(v, (int, float)) + k: not isinstance(v, bool) and isinstance(v, numbers.Real) for k, v in trial.user_attrs.items() } for attr_name, current_is_sortable in current_user_attrs.items(): diff --git a/optuna_dashboard/_note.py b/optuna_dashboard/_note.py index 95853a3d..507e3d8f 100644 --- a/optuna_dashboard/_note.py +++ b/optuna_dashboard/_note.py @@ -110,6 +110,19 @@ def note_str_key_prefix(trial_id: Optional[int]) -> str: return f"dashboard:{trial_id}:note_str:" +def copy_notes(storage: BaseStorage, src_study: optuna.Study, dst_study: optuna.Study) -> None: + system_attrs = storage.get_study_system_attrs(study_id=src_study._study_id) + + # Copy individual trial notes + for src_trial, dst_trial in zip(src_study.get_trials(), dst_study.get_trials()): + note = get_note_from_system_attrs(system_attrs, src_trial._trial_id)["body"] + save_note_with_version(storage, dst_study._study_id, dst_trial._trial_id, 0, note) + + # Copy study note + note = get_note_from_system_attrs(system_attrs, None)["body"] + save_note_with_version(storage, dst_study._study_id, None, 0, note) + + def get_note_from_system_attrs(system_attrs: dict[str, Any], trial_id: Optional[int]) -> NoteType: if note_ver_key(trial_id) not in system_attrs: return { diff --git a/optuna_dashboard/_serializer.py b/optuna_dashboard/_serializer.py index b5a3b305..7030abec 100644 --- a/optuna_dashboard/_serializer.py +++ b/optuna_dashboard/_serializer.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime import json +import numbers from typing import Any from typing import TYPE_CHECKING from typing import Union @@ -104,6 +105,8 @@ def serialize_attrs(attrs: dict[str, Any]) -> list[Attribute]: value = "" elif isinstance(v, str): value = v + elif isinstance(v, numbers.Real): + value = str(v) else: value = json.dumps(v) value = value[:MAX_ATTR_LENGTH] if len(value) > MAX_ATTR_LENGTH else value diff --git a/optuna_dashboard/artifact/_backend.py b/optuna_dashboard/artifact/_backend.py index 10a5b1ee..81cd766d 100644 --- a/optuna_dashboard/artifact/_backend.py +++ b/optuna_dashboard/artifact/_backend.py @@ -105,7 +105,7 @@ def register_artifact_route( @app.post("/api/artifacts//") @json_api_view - def upload_artifact_api(study_id: int, trial_id: int) -> dict[str, Any]: + def upload_trial_artifact_api(study_id: int, trial_id: int) -> dict[str, Any]: trial = storage.get_trial(trial_id) if trial is None: response.status = 400 @@ -139,14 +139,48 @@ def register_artifact_route( storage.set_trial_system_attr(trial_id, attr_key, json.dumps(artifact)) response.status = 201 + trial = storage.get_trial(trial_id) # Fetch trial.system_attrs again. return { "artifact_id": artifact_id, "artifacts": list_trial_artifacts(storage.get_study_system_attrs(study_id), trial), } + @app.post("/api/artifacts/") + @json_api_view + def upload_study_artifact_api(study_id: int) -> dict[str, Any]: + if artifact_store is None: + response.status = 400 # Bad Request + return {"reason": "Cannot access to the artifacts."} + file = request.json.get("file") + if file is None: + response.status = 400 + return {"reason": "Please specify the 'file' key."} + + _, data = parse_data_uri(file) + filename = request.json.get("filename", "") + artifact_id = str(uuid.uuid4()) + artifact_store.write(artifact_id, io.BytesIO(data)) + + mimetype, encoding = mimetypes.guess_type(filename) + artifact = { + "artifact_id": artifact_id, + "filename": filename, + "mimetype": mimetype or DEFAULT_MIME_TYPE, + "encoding": encoding, + } + attr_key = ARTIFACTS_ATTR_PREFIX + artifact_id + storage.set_study_system_attr(study_id, attr_key, json.dumps(artifact)) + + response.status = 201 + + return { + "artifact_id": artifact_id, + "artifacts": list_study_artifacts(storage.get_study_system_attrs(study_id)), + } + @app.delete("/api/artifacts///") @json_api_view - def delete_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]: + def delete_trial_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]: if artifact_store is None: response.status = 400 # Bad Request return {"reason": "Cannot access to the artifacts."} @@ -154,7 +188,7 @@ def register_artifact_route( # The artifact's metadata is stored in one of the following two locations: storage.set_study_system_attr( - study_id, _dashboard_trial_artifact_prefix(trial_id) + artifact_id, json.dumps(None) + study_id, _dashboard_artifact_prefix(trial_id) + artifact_id, json.dumps(None) ) storage.set_trial_system_attr( trial_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None) @@ -163,6 +197,21 @@ def register_artifact_route( response.status = 204 return {} + @app.delete("/api/artifacts//") + @json_api_view + def delete_study_artifact(study_id: int, artifact_id: str) -> dict[str, Any]: + if artifact_store is None: + response.status = 400 # Bad Request + return {"reason": "Cannot access to the artifacts."} + artifact_store.remove(artifact_id) + + storage.set_study_system_attr( + study_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None) + ) + + response.status = 204 + return {} + def upload_artifact( backend: ArtifactBackend, @@ -220,7 +269,7 @@ def upload_artifact( return artifact_id -def _dashboard_trial_artifact_prefix(trial_id: int) -> str: +def _dashboard_artifact_prefix(trial_id: int) -> str: return DASHBOARD_ARTIFACTS_ATTR_PREFIX + f"{trial_id}:" @@ -240,7 +289,7 @@ def get_trial_artifact_meta( ) -> Optional[ArtifactMeta]: # Search study_system_attrs due to backward compatibility. study_system_attrs = storage.get_study_system_attrs(study_id) - attr_key = _dashboard_trial_artifact_prefix(trial_id=trial_id) + artifact_id + attr_key = _dashboard_artifact_prefix(trial_id=trial_id) + artifact_id artifact_meta = study_system_attrs.get(attr_key) if artifact_meta is not None: return json.loads(artifact_meta) @@ -284,7 +333,7 @@ def list_trial_artifacts( dashboard_artifact_metas = [ json.loads(value) for key, value in study_system_attrs.items() - if key.startswith(_dashboard_trial_artifact_prefix(trial._trial_id)) + if key.startswith(_dashboard_artifact_prefix(trial._trial_id)) ] # Collect ArtifactMeta from trial_system_attrs. Note that artifacts uploaded via diff --git a/optuna_dashboard/preferential/_study.py b/optuna_dashboard/preferential/_study.py index d715b97b..101b61de 100644 --- a/optuna_dashboard/preferential/_study.py +++ b/optuna_dashboard/preferential/_study.py @@ -7,9 +7,9 @@ from typing import Iterable import optuna from optuna import logging +from optuna._imports import try_import from optuna.distributions import BaseDistribution from optuna.samplers import BaseSampler -from optuna.samplers import RandomSampler from optuna.trial import FrozenTrial from optuna.trial import TrialState from optuna_dashboard.preferential._system_attrs import get_n_generate @@ -20,6 +20,10 @@ from optuna_dashboard.preferential._system_attrs import report_preferences from optuna_dashboard.preferential._system_attrs import set_n_generate +with try_import() as _imports: + from optuna_dashboard.preferential.samplers.gp import PreferentialGPSampler + + _logger = logging.get_logger(__name__) _SYSTEM_ATTR_PREFERENTIAL_STUDY = "preference:is_preferential" @@ -344,11 +348,10 @@ def create_study( sampler: A sampler object that implements background algorithm for value suggestion. - If :obj:`None` is specified, `RandomSampler`_ is used. Please note that - most Optuna samplers does not work efficiently for preferential optimization. - - .. _RandomSampler: https://optuna.readthedocs.io/en/stable/reference/\ - samplers/generated/optuna.samplers.RandomSampler.html + If :obj:`None` is specified, + :class:`~optuna_dashboard.preferential.samplers.gp.PreferentialGPSampler` is used. + Please note that most Optuna samplers does not work efficiently for preferential + optimization. study_name: Study's name. If this argument is set to None, a unique name is generated @@ -369,9 +372,13 @@ def create_study( The interface may change in newer versions without prior notice. """ try: + if sampler is None: + _imports.check() # If BoTorch is not installed, raise ImportError. + sampler = PreferentialGPSampler() + study = optuna.create_study( storage=storage, - sampler=sampler or RandomSampler(), + sampler=sampler, study_name=study_name, ) study._storage.set_study_system_attr( @@ -441,11 +448,10 @@ def load_study( :func:`~optuna.study.create_study` for further details. sampler: A sampler object that implements background algorithm for value suggestion. - If :obj:`None` is specified, `RandomSampler`_ is used. Please note that - most Optuna samplers does not work efficiently for preferential optimization. - - .. _RandomSampler: https://optuna.readthedocs.io/en/stable/reference/samplers/\ - generated/optuna.samplers.RandomSampler.html + If :obj:`None` is specified, + :class:`~optuna_dashboard.preferential.samplers.gp.PreferentialGPSampler` is used. + Please note that most Optuna samplers does not work efficiently for preferential + optimization. Returns: A :class:`~optuna_dashboard.preferential.PreferentialStudy` object. @@ -454,9 +460,11 @@ def load_study( Preferential optimization is an experimental feature (introduced in v0.13.0). The interface may change in newer versions without prior notice. """ - study = optuna.load_study( - study_name=study_name, storage=storage, sampler=sampler or RandomSampler() - ) + if sampler is None: + _imports.check() # If BoTorch is not installed, raise ImportError. + sampler = PreferentialGPSampler() + + study = optuna.load_study(study_name=study_name, storage=storage, sampler=sampler) system_attrs = study._storage.get_study_system_attrs(study._study_id) if not system_attrs.get(_SYSTEM_ATTR_PREFERENTIAL_STUDY): raise ValueError("The study is not a PreferentialStudy.") diff --git a/optuna_dashboard/preferential/samplers/gp.py b/optuna_dashboard/preferential/samplers/gp.py index 2e782b68..590c5570 100644 --- a/optuna_dashboard/preferential/samplers/gp.py +++ b/optuna_dashboard/preferential/samplers/gp.py @@ -50,15 +50,15 @@ def _orthants_MVN_Gibbs_sampling(cov_inv: Tensor, cycles: int, initial_sample: T def _one_side_trunc_norm_sampling(lower: Tensor) -> Tensor: - if lower > 4.0: - r = torch.clamp_min(torch.rand(torch.Size(()), dtype=torch.float64), min=1e-300) - return (lower * lower - 2 * r.log()).sqrt() - else: - SQRT2 = math.sqrt(2) - r = torch.rand(torch.Size(()), dtype=torch.float64) * torch.erfc(lower / SQRT2) - while 1 - r == 1: - r = torch.rand(torch.Size(()), dtype=torch.float64) * torch.erfc(lower / SQRT2) - return torch.erfinv(1 - r) * SQRT2 + r = torch.rand(torch.Size(()), dtype=torch.float64) + ret = -torch.special.ndtri(torch.exp(torch.special.log_ndtr(-lower) + r.log())) + + # If sampled random number is very small, `ret` becomes inf. + while torch.isinf(ret): + r = torch.rand(torch.Size(()), dtype=torch.float64) + ret = -torch.special.ndtri(torch.exp(torch.special.log_ndtr(-lower) + r.log())) + + return ret _orthants_MVN_Gibbs_sampling_jit = torch.jit.script(_orthants_MVN_Gibbs_sampling) @@ -284,6 +284,26 @@ class _PreferentialGP: class PreferentialGPSampler(optuna.samplers.BaseSampler): + """Sampler for preferential optimization using Gaussian process. + + The sampling algorithm is based on `Takeno et al., 2023 `_. + This sampler uses BoTorch to optimize acquisition function. + + Args: + kernel: + Kernel that computes the covariance on the Gaussian process. Defaults to + Matern 3/2 Kernel + ARD. + noise_prior: + Prior of the observation noise. Defaults to gamma prior. + independent_sampler: + A :class:`~optuna.samplers.BaseSampler` instance that is used for independent + sampling. The parameters not contained in the relative search space are sampled + by this sampler. If :obj:`None` is specified, + :class:`~optuna.samplers.RandomSampler` is used as the default. + seed: + Seed for random number generator. + """ + def __init__( self, *, @@ -297,7 +317,7 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler): self._rng = np.random.RandomState(seed) self.independent_sampler = independent_sampler or optuna.samplers.RandomSampler( - seed=self._rng.randint(2**32) + seed=self._rng.randint(2**32, dtype=np.int64) ) self._search_space = optuna.search_space.IntersectionSearchSpace() @@ -335,7 +355,7 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler): ) pref_ids = torch.tensor([[ids[b], ids[w]] for b, w in preferences], dtype=torch.int32) with torch.random.fork_rng(): - torch.manual_seed(self._rng.randint(2**32)) + torch.manual_seed(self._rng.randint(2**32, dtype=np.int64)) self._gp = self._gp or _PreferentialGP( kernel=self.kernel diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index 41afdadb..3c8f06b6 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -11,9 +11,11 @@ import { tellTrialAPI, saveTrialUserAttrsAPI, renameStudyAPI, - uploadArtifactAPI, + uploadTrialArtifactAPI, + uploadStudyArtifactAPI, getMetaInfoAPI, - deleteArtifactAPI, + deleteTrialArtifactAPI, + deleteStudyArtifactAPI, reportPreferenceAPI, skipPreferentialTrialAPI, removePreferentialHistoryAPI, @@ -100,7 +102,13 @@ export const actionCreator = () => { setTrial(studyId, trialIndex, newTrial) } - const deleteTrialArtifact = ( + const setStudyArtifacts = (studyId: number, artifacts: Artifact[]) => { + const newStudy: StudyDetail = Object.assign({}, studyDetails[studyId]) + newStudy.artifacts = artifacts + setStudyDetailState(studyId, newStudy) + } + + const deleteTrialArtifactState = ( studyId: number, trialId: number, artifact_id: string @@ -122,6 +130,18 @@ export const actionCreator = () => { setTrialArtifacts(studyId, index, newArtifacts) } + const deleteStudyArtifactState = (studyId: number, artifact_id: string) => { + const artifacts = studyDetails[studyId].artifacts + const artifactIndex = artifacts.findIndex( + (a) => a.artifact_id === artifact_id + ) + const newArtifacts = [ + ...artifacts.slice(0, artifactIndex), + ...artifacts.slice(artifactIndex + 1, artifacts.length), + ] + setStudyArtifacts(studyId, newArtifacts) + } + const setTrialStateValues = ( studyId: number, index: number, @@ -154,7 +174,7 @@ export const actionCreator = () => { currentValue > bestValue ) { newStudy.best_trials = [newTrial] - } else if (currentValue == bestValue) { + } else if (currentValue === bestValue) { newStudy.best_trials = [...newStudy.best_trials, newTrial] } } @@ -430,7 +450,7 @@ export const actionCreator = () => { }) } - const uploadArtifact = ( + const uploadTrialArtifact = ( studyId: number, trialId: number, file: File @@ -439,7 +459,7 @@ export const actionCreator = () => { setUploading(true) reader.readAsDataURL(file) reader.onload = (upload: ProgressEvent) => { - uploadArtifactAPI( + uploadTrialArtifactAPI( studyId, trialId, file.name, @@ -467,14 +487,56 @@ export const actionCreator = () => { } } - const deleteArtifact = ( + const uploadStudyArtifact = (studyId: number, file: File): void => { + const reader = new FileReader() + setUploading(true) + reader.readAsDataURL(file) + reader.onload = (upload: ProgressEvent) => { + uploadStudyArtifactAPI( + studyId, + file.name, + upload.target?.result as string + ) + .then((res) => { + setUploading(false) + setStudyArtifacts(studyId, res.artifacts) + }) + .catch((err) => { + setUploading(false) + const reason = err.response?.data.reason + enqueueSnackbar(`Failed to upload ${reason}`, { variant: "error" }) + }) + } + reader.onerror = (error) => { + enqueueSnackbar(`Failed to read the file ${error}`, { variant: "error" }) + console.log(error) + } + } + + const deleteTrialArtifact = ( studyId: number, trialId: number, artifactId: string ): void => { - deleteArtifactAPI(studyId, trialId, artifactId) + deleteTrialArtifactAPI(studyId, trialId, artifactId) .then(() => { - deleteTrialArtifact(studyId, trialId, artifactId) + deleteTrialArtifactState(studyId, trialId, artifactId) + enqueueSnackbar(`Success to delete an artifact.`, { + variant: "success", + }) + }) + .catch((err) => { + const reason = err.response?.data.reason + enqueueSnackbar(`Failed to delete ${reason}.`, { + variant: "error", + }) + }) + } + + const deleteStudyArtifact = (studyId: number, artifactId: string): void => { + deleteStudyArtifactAPI(studyId, artifactId) + .then(() => { + deleteStudyArtifactState(studyId, artifactId) enqueueSnackbar(`Success to delete an artifact.`, { variant: "success", }) @@ -693,8 +755,10 @@ export const actionCreator = () => { saveReloadInterval, saveStudyNote, saveTrialNote, - uploadArtifact, - deleteArtifact, + uploadTrialArtifact, + uploadStudyArtifact, + deleteTrialArtifact, + deleteStudyArtifact, makeTrialComplete, makeTrialFail, saveTrialUserAttrs, diff --git a/optuna_dashboard/ts/apiClient.ts b/optuna_dashboard/ts/apiClient.ts index e5510d67..f42d20de 100644 --- a/optuna_dashboard/ts/apiClient.ts +++ b/optuna_dashboard/ts/apiClient.ts @@ -280,7 +280,7 @@ type UploadArtifactAPIResponse = { artifacts: Artifact[] } -export const uploadArtifactAPI = ( +export const uploadTrialArtifactAPI = ( studyId: number, trialId: number, fileName: string, @@ -296,7 +296,22 @@ export const uploadArtifactAPI = ( }) } -export const deleteArtifactAPI = ( +export const uploadStudyArtifactAPI = ( + studyId: number, + fileName: string, + dataUrl: string +): Promise => { + return axiosInstance + .post(`/api/artifacts/${studyId}`, { + file: dataUrl, + filename: fileName, + }) + .then((res) => { + return res.data + }) +} + +export const deleteTrialArtifactAPI = ( studyId: number, trialId: number, artifactId: string @@ -308,6 +323,17 @@ export const deleteArtifactAPI = ( }) } +export const deleteStudyArtifactAPI = ( + studyId: number, + artifactId: string +): Promise => { + return axiosInstance + .delete(`/api/artifacts/${studyId}/${artifactId}`) + .then(() => { + return + }) +} + export const tellTrialAPI = ( trialId: number, state: TrialStateFinished, diff --git a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx index 6326cb16..fecef837 100644 --- a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx +++ b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx @@ -3,8 +3,9 @@ import { ThreejsArtifactViewer, isThreejsArtifact, } from "./ThreejsArtifactViewer" +import { WaveSurferArtifactViewer } from "./WaveSurferArtifactViewer" import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile" -import { CardMedia } from "@mui/material" +import { CardMedia, Box } from "@mui/material" export const ArtifactCardMedia: FC<{ artifact: Artifact @@ -35,9 +36,21 @@ export const ArtifactCardMedia: FC<{ ) } else if (artifact.mimetype.startsWith("audio")) { return ( - + + + ) } else if (artifact.mimetype.startsWith("image")) { return ( diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index 2d6a5670..adf64e77 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -10,12 +10,16 @@ import { TableSortLabel, Collapse, IconButton, - useTheme, + Menu, + MenuItem, } from "@mui/material" import { styled } from "@mui/system" import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown" import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp" -import { Clear } from "@mui/icons-material" +import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank" +import CheckBoxIcon from "@mui/icons-material/CheckBox" +import FilterListIcon from "@mui/icons-material/FilterList" +import ListItemIcon from "@mui/material/ListItemIcon" type Order = "asc" | "desc" @@ -28,15 +32,15 @@ interface DataGridColumn { field: keyof T label: string sortable?: boolean - less?: (a: T, b: T) => number - filterable?: boolean + less?: (a: T, b: T, ascending: boolean) => number + filterChoices?: string[] toCellValue?: (rowIndex: number) => string | React.ReactNode padding?: "normal" | "checkbox" | "none" } interface RowFilter { columnIdx: number - value: Value + values: Value[] } function DataGrid(props: { @@ -81,28 +85,13 @@ function DataGrid(props: { } // Filtering - const fieldAlreadyFiltered = (columnIdx: number): boolean => - filters.some((f) => f.columnIdx === columnIdx) - - const handleClickFilterCell = (columnIdx: number, value: Value) => { - if (fieldAlreadyFiltered(columnIdx)) { - return - } - const newFilters = [...filters, { columnIdx: columnIdx, value: value }] - setFilters(newFilters) - } - - const clearFilter = (columnIdx: number): void => { - setFilters(filters.filter((f) => f.columnIdx !== columnIdx)) - } - const filteredRows = rows.filter((row, rowIdx) => { if (defaultFilter !== undefined && defaultFilter(row)) { return false } return filters.length === 0 ? true - : filters.some((f) => { + : filters.every((f) => { if (columns.length <= f.columnIdx) { console.log( `columnIdx=${f.columnIdx} must be smaller than columns.length=${columns.length}` @@ -110,20 +99,15 @@ function DataGrid(props: { return true } const toCellValue = columns[f.columnIdx].toCellValue - if (toCellValue !== undefined) { - return toCellValue(rowIdx) === f.value - } - const field = columns[f.columnIdx].field - return row[field] === f.value + const cellValue = + toCellValue !== undefined + ? toCellValue(rowIdx) + : row[columns[f.columnIdx].field] + return f.values.some((v) => v === cellValue) }) }) // Sorting - const createSortHandler = (columnId: number) => () => { - const isAsc = orderBy === columnId && order === "asc" - setOrder(isAsc ? "desc" : "asc") - setOrderBy(columnId) - } const sortedRows = stableSort(filteredRows, order, orderBy, columns) const currentPageRows = rowsPerPage > 0 @@ -135,20 +119,6 @@ function DataGrid(props: { const RootDiv = styled("div")({ width: "100%", }) - const HiddenSpan = styled("span")({ - border: 0, - clip: "rect(0 0 0 0)", - height: 1, - margin: -1, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 20, - width: 1, - }) - const TableHeaderCellSpan = styled("span")({ - display: "inline-flex", - }) return ( @@ -160,50 +130,32 @@ function DataGrid(props: { {collapseBody ? : null} - {columns.map((column, columnIdx) => ( - - - {column.sortable ? ( - - {column.label} - {orderBy === column.field ? ( - - {order === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - ) : ( - column.label - )} - {column.filterable ? ( - { - clearFilter(columnIdx) - }} - > - - - ) : null} - - - ))} + {columns.map((column, columnIdx) => { + return ( + + key={columnIdx} + column={column} + order={orderBy === columnIdx ? order : null} + filter={ + filters.find((f) => f.columnIdx === columnIdx) || null + } + onOrderByChange={(direction: Order) => { + setOrder(direction) + setOrderBy(columnIdx) + }} + onFilterChange={(values: Value[]) => { + const newFilters = filters.filter( + (f) => f.columnIdx !== columnIdx + ) + newFilters.push({ + columnIdx: columnIdx, + values: values, + }) + setFilters(newFilters) + }} + /> + ) + })} @@ -215,7 +167,6 @@ function DataGrid(props: { keyField={keyField} collapseBody={collapseBody} key={`${row[keyField]}`} - handleClickFilterCell={handleClickFilterCell} /> ))} {emptyRows > 0 && ( @@ -239,30 +190,119 @@ function DataGrid(props: { ) } +const TableHeaderCellSpan = styled("span")({ + display: "inline-flex", +}) + +const HiddenSpan = styled("span")({ + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, +}) + +function DataGridHeaderColumn(props: { + column: DataGridColumn + order: Order | null + onOrderByChange: (order: Order) => void + filter: RowFilter | null + onFilterChange: (values: Value[]) => void + dense?: boolean +}) { + const { column, order, onOrderByChange, filter, onFilterChange, dense } = + props + const [filterMenuAnchorEl, setFilterMenuAnchorEl] = + React.useState(null) + + const filterChoices = column.filterChoices + + return ( + + + {column.sortable ? ( + { + onOrderByChange(order === "asc" ? "desc" : "asc") + }} + > + {column.label} + {order !== null ? ( + + {order === "desc" ? "sorted descending" : "sorted ascending"} + + ) : null} + + ) : ( + column.label + )} + {filterChoices !== undefined ? ( + <> + { + setFilterMenuAnchorEl(e.currentTarget) + }} + > + + + { + setFilterMenuAnchorEl(null) + }} + > + {filterChoices.map((choice) => ( + { + const newTickedValues = + filter === null + ? filterChoices.filter((v) => v !== choice) // By default, every choice is ticked, so the chosen option will be unticked. + : filter.values.some((v) => v === choice) + ? filter.values.filter((v) => v !== choice) + : [...filter.values, choice] + onFilterChange(newTickedValues) + }} + > + + {!filter || filter.values.some((v) => v === choice) ? ( + + ) : ( + + )} + + {choice} + + ))} + + + ) : null} + + + ) +} + function DataGridRow(props: { columns: DataGridColumn[] rowIndex: number row: T keyField: keyof T collapseBody?: (rowIndex: number) => React.ReactNode - handleClickFilterCell: (columnIdx: number, value: Value) => void }) { - const { - columns, - rowIndex, - row, - keyField, - collapseBody, - handleClickFilterCell, - } = props + const { columns, rowIndex, row, keyField, collapseBody } = props const [open, setOpen] = React.useState(false) - const theme = useTheme() - const FilterableDiv = styled("div")({ - color: theme.palette.primary.main, - textDecoration: "underline", - cursor: "pointer", - }) return ( @@ -283,21 +323,7 @@ function DataGridRow(props: { : // TODO(c-bata): Avoid this implicit type conversion. (row[column.field] as number | string | null | undefined) - return column.filterable ? ( - { - const value = - column.toCellValue !== undefined - ? column.toCellValue(rowIndex) - : row[column.field] - handleClickFilterCell(columnIndex, value) - }} - > - {cellItem} - - ) : ( + return ( ( const stabilizedThis = array.map((el, index) => [el, index] as [T, number]) stabilizedThis.sort((a, b) => { if (less) { - const result = order == "asc" ? -less(a[0], b[0]) : less(a[0], b[0]) + const ascending = order === "asc" + const result = ascending + ? -less(a[0], b[0], ascending) + : less(a[0], b[0], ascending) if (result !== 0) return result } else { const result = comparator(a[0], b[0]) diff --git a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx index 2c9c229e..7ac46162 100644 --- a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx +++ b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from "react" +import React, { ReactNode, useState, FC } from "react" import { Dialog, DialogTitle, @@ -9,7 +9,7 @@ import { } from "@mui/material" import { actionCreator } from "../action" -export const useDeleteArtifactDialog = (): [ +export const useDeleteTrialArtifactDialog = (): [ (studyId: number, trialId: number, artifact: Artifact) => void, () => ReactNode ] => { @@ -33,7 +33,7 @@ export const useDeleteArtifactDialog = (): [ if (artifact === null) { return } - action.deleteArtifact(studyId, trialId, artifact.artifact_id) + action.deleteTrialArtifact(studyId, trialId, artifact.artifact_id) setOpenDeleteArtifactDialog(false) setTarget([-1, -1, null]) } @@ -45,32 +45,96 @@ export const useDeleteArtifactDialog = (): [ const renderDeleteArtifactDialog = () => { return ( - { - handleCloseDeleteArtifactDialog() - }} - aria-labelledby="delete-artifact-dialog-title" - > - - Delete artifact - - - - Are you sure you want to delete an artifact (" - {target[2]?.filename}")? - - - - - - - + ) } return [openDialog, renderDeleteArtifactDialog] } + +export const useDeleteStudyArtifactDialog = (): [ + (studyId: number, artifact: Artifact) => void, + () => ReactNode +] => { + const action = actionCreator() + + const [openDeleteArtifactDialog, setOpenDeleteArtifactDialog] = + useState(false) + const [target, setTarget] = useState<[number, Artifact | null]>([-1, null]) + + const handleCloseDeleteArtifactDialog = () => { + setOpenDeleteArtifactDialog(false) + setTarget([-1, null]) + } + + const handleDeleteArtifact = () => { + const [studyId, artifact] = target + if (artifact === null) { + return + } + action.deleteStudyArtifact(studyId, artifact.artifact_id) + setOpenDeleteArtifactDialog(false) + setTarget([-1, null]) + } + + const openDialog = (studyId: number, artifact: Artifact) => { + setTarget([studyId, artifact]) + setOpenDeleteArtifactDialog(true) + } + + const renderDeleteArtifactDialog = () => { + return ( + + ) + } + return [openDialog, renderDeleteArtifactDialog] +} + +const DeleteDialog: FC<{ + openDeleteArtifactDialog: boolean + handleCloseDeleteArtifactDialog: () => void + filename: string | undefined + handleDeleteArtifact: () => void +}> = ({ + openDeleteArtifactDialog, + handleCloseDeleteArtifactDialog, + filename, + handleDeleteArtifact, +}) => { + return ( + { + handleCloseDeleteArtifactDialog() + }} + aria-labelledby="delete-artifact-dialog-title" + > + + Delete artifact + + + + Are you sure you want to delete an artifact (" + {filename}")? + + + + + + + + ) +} diff --git a/optuna_dashboard/ts/components/GraphContour.tsx b/optuna_dashboard/ts/components/GraphContour.tsx index b186fffe..b7895ddf 100644 --- a/optuna_dashboard/ts/components/GraphContour.tsx +++ b/optuna_dashboard/ts/components/GraphContour.tsx @@ -14,25 +14,8 @@ import { import blue from "@mui/material/colors/blue" import { plotlyDarkTemplate } from "./PlotlyDarkMode" import { useMergedUnionSearchSpace } from "../searchSpace" +import { getAxisInfo } from "../graphUtil" -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const unique = (array: any[]) => { - const knownElements = new Map() - array.forEach((elem) => knownElements.set(elem, true)) - return Array.from(knownElements.keys()) -} - -type AxisInfo = { - name: string - min: number - max: number - isLog: boolean - isCat: boolean - indices: (string | number)[] - values: (string | number | null)[] -} - -const PADDING_RATIO = 0.05 const plotDomId = "graph-contour" export const Contour: FC<{ @@ -195,12 +178,16 @@ const plotContour = ( const xValues: plotly.Datum[] = [] const yValues: plotly.Datum[] = [] const zValues: plotly.Datum[][] = new Array(yIndices.length) + const feasibleXY = new Set() for (let j = 0; j < yIndices.length; j++) { zValues[j] = new Array(xIndices.length).fill(null) } filteredTrials.forEach((trial, i) => { if (xAxis.values[i] && yAxis.values[i] && trial.values) { + if (trial.constraints.every((c) => c <= 0)) { + feasibleXY.add(xValues.length) + } const xValue = xAxis.values[i] as string | number const yValue = yAxis.values[i] as string | number xValues.push(xValue) @@ -234,12 +221,20 @@ const plotContour = ( }, { type: "scatter", - x: xValues, - y: yValues, + x: xValues.filter((_, i) => feasibleXY.has(i)), + y: yValues.filter((_, i) => feasibleXY.has(i)), marker: { line: { width: 2.0, color: "Grey" }, color: "black" }, mode: "markers", showlegend: false, }, + { + type: "scatter", + x: xValues.filter((_, i) => !feasibleXY.has(i)), + y: yValues.filter((_, i) => !feasibleXY.has(i)), + marker: { line: { width: 2.0, color: "Grey" }, color: "#cccccc" }, + mode: "markers", + showlegend: false, + }, ] plotly.react(plotDomId, plotData, layout) return @@ -272,93 +267,3 @@ const plotContour = ( ] plotly.react(plotDomId, plotData, layout) } - -const getAxisInfoForNumericalParams = ( - trials: Trial[], - paramName: string, - distribution: FloatDistribution | IntDistribution -): AxisInfo => { - let min = 0 - let max = 0 - if (distribution.log) { - const padding = - (Math.log10(distribution.high) - Math.log10(distribution.low)) * - PADDING_RATIO - min = Math.pow(10, Math.log10(distribution.low) - padding) - max = Math.pow(10, Math.log10(distribution.high) + padding) - } else { - const padding = (distribution.high - distribution.low) * PADDING_RATIO - min = distribution.low - padding - max = distribution.high + padding - } - - const values = trials.map( - (trial) => - trial.params.find((p) => p.name === paramName)?.param_internal_value || - null - ) - const indices = unique(values) - .filter((v) => v !== null) - .sort((a, b) => a - b) - if (indices.length >= 2) { - indices.unshift(min) - indices.push(max) - } - return { - name: paramName, - min, - max, - isLog: distribution.log, - isCat: false, - indices, - values, - } -} - -const getAxisInfoForCategoricalParams = ( - trials: Trial[], - paramName: string, - distribution: CategoricalDistribution -): AxisInfo => { - const values = trials.map( - (trial) => - trial.params.find((p) => p.name === paramName)?.param_external_value || - null - ) - const isDynamic = values.some((v) => v === null) - const span = distribution.choices.length - (isDynamic ? 2 : 1) - const padding = span * PADDING_RATIO - const min = -padding - const max = span + padding - - const indices = distribution.choices - .map((c) => c.value) - .sort((a, b) => - a.toLowerCase() < b.toLowerCase() - ? -1 - : a.toLowerCase() > b.toLowerCase() - ? 1 - : 0 - ) - return { - name: paramName, - min, - max, - isLog: false, - isCat: true, - indices, - values, - } -} - -const getAxisInfo = (trials: Trial[], param: SearchSpaceItem): AxisInfo => { - if (param.distribution.type === "CategoricalDistribution") { - return getAxisInfoForCategoricalParams( - trials, - param.name, - param.distribution - ) - } else { - return getAxisInfoForNumericalParams(trials, param.name, param.distribution) - } -} diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index ca0bfda0..a06832d9 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -78,22 +78,27 @@ const plotIntermediateValue = ( t.state === "Pruned" && t.values && t.values.length > 0) || - t.state == "Running" + t.state === "Running" ) const plotData: Partial[] = filteredTrials.map((trial) => { const values = trial.intermediate_values.filter( (iv) => iv.value !== "inf" && iv.value !== "-inf" && iv.value !== "nan" ) + const isFeasible = trial.constraints.every((c) => c <= 0) return { x: values.map((iv) => iv.step), y: values.map((iv) => iv.value), marker: { maxdisplayed: 10 }, mode: "lines+markers", type: "scatter", - name: - trial.state !== "Running" - ? `trial #${trial.number}` - : `trial #${trial.number} (running)`, + name: `trial #${trial.number} ${ + trial.state === "Running" + ? "(running)" + : !isFeasible + ? "(infeasible)" + : "" + }`, + ...(!isFeasible && { line: { color: "#CCCCCC" } }), } }) plotly.react(plotDomId, plotData, layout) diff --git a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx index 71f2b32d..b991ef94 100644 --- a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx +++ b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx @@ -164,7 +164,7 @@ const plotCoordinate = ( return truncated .split("") .map((c, i) => { - return (i + 1) % breakLength == 0 ? c + "
" : c + return (i + 1) % breakLength === 0 ? c + "
" : c }) .join("") } diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx new file mode 100644 index 00000000..6daaf10a --- /dev/null +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -0,0 +1,342 @@ +import * as plotly from "plotly.js-dist-min" +import React, { FC, useEffect, useState } from "react" +import { + Grid, + FormControl, + FormLabel, + MenuItem, + Select, + Typography, + SelectChangeEvent, + useTheme, + Box, +} from "@mui/material" +import { plotlyDarkTemplate } from "./PlotlyDarkMode" +import { getAxisInfo, makeHovertext } from "../graphUtil" +import { useMergedUnionSearchSpace } from "../searchSpace" + +const plotDomId = "graph-rank" + +interface RankPlotInfo { + xtitle: string + ytitle: string + xtype: plotly.AxisType + ytype: plotly.AxisType + xvalues: (string | number)[] + yvalues: (string | number)[] + colors: number[] + is_feasible: boolean[] + hovertext: string[] +} + +export const GraphRank: FC<{ + study: StudyDetail | null +}> = ({ study = null }) => { + const theme = useTheme() + const [objectiveId, setobjectiveId] = useState(0) + const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) + const [xParam, setXParam] = useState(null) + const [yParam, setYParam] = useState(null) + const objectiveNames: string[] = study?.objective_names || [] + + if (xParam === null && searchSpace.length > 0) { + setXParam(searchSpace[0]) + } + if (yParam === null && searchSpace.length > 1) { + setYParam(searchSpace[1]) + } + + const handleObjectiveChange = (event: SelectChangeEvent) => { + setobjectiveId(Number(event.target.value)) + } + const handleXParamChange = (event: SelectChangeEvent) => { + const param = searchSpace.find((item) => item.name === event.target.value) + setXParam(param || null) + } + const handleYParamChange = (event: SelectChangeEvent) => { + const param = searchSpace.find((item) => item.name === event.target.value) + setYParam(param || null) + } + + useEffect(() => { + if (study != null) { + const rankPlotInfo = getRankPlotInfo(study, objectiveId, xParam, yParam) + plotRank(rankPlotInfo, theme.palette.mode) + } + }, [study, objectiveId, xParam, yParam, theme.palette.mode]) + + const space: SearchSpaceItem[] = study ? study.union_search_space : [] + + return ( + + + + Rank + + {study !== null && study.directions.length !== 1 ? ( + + Objective: + + + ) : null} + {study !== null && space.length > 0 ? ( + + + x: + + + + y: + + + + ) : null} + + + + + + ) +} + +const getRankPlotInfo = ( + study: StudyDetail | null, + objectiveId: number, + xParam: SearchSpaceItem | null, + yParam: SearchSpaceItem | null +): RankPlotInfo | null => { + if (study === null) { + return null + } + + const trials = study.trials + const filteredTrials = trials.filter(filterFunc) + if (filteredTrials.length < 2 || xParam === null || yParam === null) { + return null + } + + const xAxis = getAxisInfo(filteredTrials, xParam) + const yAxis = getAxisInfo(filteredTrials, yParam) + + let xValues: (string | number)[] = [] + let yValues: (string | number)[] = [] + const zValues: number[] = [] + const isFeasible: boolean[] = [] + const hovertext: string[] = [] + const convertTrialValueToNumber = (value: TrialValueNumber): number => { + // TrialValueNumber takes `number`, "inf", or "-inf". + return typeof value === "number" + ? value + : value.includes("-") + ? -Infinity + : Infinity + } + filteredTrials.forEach((trial, i) => { + const xValue = xAxis.values[i] + const yValue = yAxis.values[i] + if (xValue && yValue && trial.values) { + xValues.push(xValue) + yValues.push(yValue) + const zValue = convertTrialValueToNumber(trial.values[objectiveId]) + zValues.push(zValue) + const feasibility = trial.constraints.every((c) => c <= 0) + isFeasible.push(feasibility) + hovertext.push(makeHovertext(trial)) + } + }) + + const colors = getColors(zValues) + + if (xAxis.isCat && !yAxis.isCat) { + const indices: number[] = Array.from(Array(xValues.length).keys()).sort( + (a, b) => + xValues[a] + .toString() + .toLowerCase() + .localeCompare(xValues[b].toString().toLowerCase()) + ) + xValues = indices.map((i) => xValues[i]) + yValues = indices.map((i) => yValues[i]) + } else if (!xAxis.isCat && yAxis.isCat) { + const indices: number[] = Array.from(Array(yValues.length).keys()).sort( + (a, b) => + yValues[a] + .toString() + .toLowerCase() + .localeCompare(yValues[b].toString().toLowerCase()) + ) + xValues = indices.map((i) => xValues[i]) + yValues = indices.map((i) => yValues[i]) + } else if (xAxis.isCat && yAxis.isCat) { + const indices: number[] = Array.from(Array(xValues.length).keys()).sort( + (a, b) => { + const xComp = xValues[a] + .toString() + .toLowerCase() + .localeCompare(xValues[b].toString().toLowerCase()) + if (xComp !== 0) { + return xComp + } + return yValues[a] + .toString() + .toLowerCase() + .localeCompare(yValues[b].toString().toLowerCase()) + } + ) + xValues = indices.map((i) => xValues[i]) + yValues = indices.map((i) => yValues[i]) + } + + return { + xtitle: xAxis.name, + ytitle: yAxis.name, + xtype: xAxis.isCat ? "category" : xAxis.isLog ? "log" : "linear", + ytype: yAxis.isCat ? "category" : yAxis.isLog ? "log" : "linear", + xvalues: xValues, + yvalues: yValues, + colors, + is_feasible: isFeasible, + hovertext, + } +} + +const filterFunc = (trial: Trial): boolean => { + return trial.state === "Complete" && trial.values !== undefined +} + +const getColors = (values: number[]): number[] => { + const rawRanks = getOrderWithSameOrderAveraging(values) + let colorIdxs: number[] = [] + if (values.length > 2) { + colorIdxs = rawRanks.map((rank) => rank / (values.length - 1)) + } else { + colorIdxs = [0.5] + } + return colorIdxs +} + +const getOrderWithSameOrderAveraging = (values: number[]): number[] => { + const sortedValues = values.slice().sort((a, b) => a - b) + const ranks: number[] = [] + values.forEach((value) => { + const firstIndex = sortedValues.indexOf(value) + const lastIndex = sortedValues.lastIndexOf(value) + const sumOfTheValue = + ((firstIndex + lastIndex) * (lastIndex - firstIndex + 1)) / 2 + const rank = sumOfTheValue / (lastIndex - firstIndex + 1) + ranks.push(rank) + }) + return ranks +} + +const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { + if (document.getElementById(plotDomId) === null) { + return + } + + if (rankPlotInfo === null) { + plotly.react(plotDomId, [], { + template: mode === "dark" ? plotlyDarkTemplate : {}, + }) + return + } + + const layout: Partial = { + xaxis: { + title: rankPlotInfo.xtitle, + type: rankPlotInfo.xtype, + }, + yaxis: { + title: rankPlotInfo.ytitle, + type: rankPlotInfo.ytype, + }, + margin: { + l: 50, + t: 0, + r: 50, + b: 50, + }, + uirevision: "true", + template: mode === "dark" ? plotlyDarkTemplate : {}, + } + + const xValues = rankPlotInfo.xvalues + const yValues = rankPlotInfo.yvalues + + const plotData: Partial[] = [ + { + type: "scatter", + x: xValues.filter((_, i) => rankPlotInfo.is_feasible[i]), + y: yValues.filter((_, i) => rankPlotInfo.is_feasible[i]), + marker: { + color: rankPlotInfo.colors.filter( + (_, i) => rankPlotInfo.is_feasible[i] + ), + colorscale: "Portland", + colorbar: { + title: "Rank", + }, + size: 10, + line: { + color: "Grey", + width: 0.5, + }, + }, + mode: "markers", + showlegend: false, + hovertemplate: "%{hovertext}", + hovertext: rankPlotInfo.hovertext.filter( + (_, i) => rankPlotInfo.is_feasible[i] + ), + }, + { + type: "scatter", + x: xValues.filter((_, i) => !rankPlotInfo.is_feasible[i]), + y: yValues.filter((_, i) => !rankPlotInfo.is_feasible[i]), + marker: { + color: "#cccccc", + size: 10, + line: { + color: "Grey", + width: 0.5, + }, + }, + mode: "markers", + showlegend: false, + hovertemplate: "%{hovertext}", + hovertext: rankPlotInfo.hovertext.filter( + (_, i) => !rankPlotInfo.is_feasible[i] + ), + }, + ] + plotly.react(plotDomId, plotData, layout) +} diff --git a/optuna_dashboard/ts/components/GraphSlice.tsx b/optuna_dashboard/ts/components/GraphSlice.tsx index f37a5413..2a807cb7 100644 --- a/optuna_dashboard/ts/components/GraphSlice.tsx +++ b/optuna_dashboard/ts/components/GraphSlice.tsx @@ -188,65 +188,71 @@ const plotSlice = ( return } - const objectiveValues: number[] = trials.map( + const feasibleTrials: Trial[] = [] + const infeasibleTrials: Trial[] = [] + trials.forEach((t) => { + if (t.constraints.every((c) => c <= 0)) { + feasibleTrials.push(t) + } else { + infeasibleTrials.push(t) + } + }) + + const feasibleObjectiveValues: number[] = feasibleTrials.map( (t) => objectiveTarget.getTargetValue(t) as number ) - const values = trials.map( - (t) => selectedParamTarget.getTargetValue(t) as number + const infeasibleObjectiveValues: number[] = infeasibleTrials.map( + (t) => objectiveTarget.getTargetValue(t) as number ) - const trialNumbers: number[] = trials.map((t) => t.number) - if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { - const trace: plotly.Data[] = [ - { - type: "scatter", - x: values, - y: objectiveValues, - mode: "markers", - marker: { - color: trialNumbers, - colorscale: "Blues", - reversescale: true, - colorbar: { - title: "Trial", - }, - line: { - color: "Grey", - width: 0.5, - }, + const feasibleValues = feasibleTrials.map( + (t) => selectedParamTarget.getTargetValue(t) as number + ) + const infeasibleValues = infeasibleTrials.map( + (t) => selectedParamTarget.getTargetValue(t) as number + ) + const trace: plotly.Data[] = [ + { + type: "scatter", + x: feasibleValues, + y: feasibleObjectiveValues, + mode: "markers", + name: "Feasible Trial", + marker: { + color: feasibleTrials.map((t) => t.number), + colorscale: "Blues", + reversescale: true, + colorbar: { + title: "Trial", + }, + line: { + color: "Grey", + width: 0.5, }, }, - ] + }, + { + type: "scatter", + x: infeasibleValues, + y: infeasibleObjectiveValues, + mode: "markers", + name: "Infeasible Trial", + marker: { + color: "#cccccc", + reversescale: true, + }, + }, + ] + if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { layout["xaxis"] = { title: selectedParamTarget.toLabel(), type: isLogScale(selectedParamSpace) ? "log" : "linear", gridwidth: 1, automargin: true, // Otherwise the label is outside of the plot } - plotly.react(plotDomId, trace, layout) } else { const vocabArr = selectedParamSpace.distribution.choices.map((c) => c.value) const tickvals: number[] = vocabArr.map((v, i) => i) - const trace: plotly.Data[] = [ - { - type: "scatter", - x: values, - y: objectiveValues, - mode: "markers", - marker: { - color: trialNumbers, - colorscale: "Blues", - reversescale: true, - colorbar: { - title: "Trial", - }, - line: { - color: "Grey", - width: 0.5, - }, - }, - }, - ] layout["xaxis"] = { title: selectedParamTarget.toLabel(), type: "linear", @@ -255,6 +261,6 @@ const plotSlice = ( ticktext: vocabArr, automargin: true, // Otherwise the label is outside of the plot } - plotly.react(plotDomId, trace, layout) } + plotly.react(plotDomId, trace, layout) } diff --git a/optuna_dashboard/ts/components/GraphTimeline.tsx b/optuna_dashboard/ts/components/GraphTimeline.tsx index 68afe0fc..cc0f7f57 100644 --- a/optuna_dashboard/ts/components/GraphTimeline.tsx +++ b/optuna_dashboard/ts/components/GraphTimeline.tsx @@ -92,12 +92,7 @@ const plotTimeline = (trials: Trial[], mode: string) => { template: mode === "dark" ? plotlyDarkTemplate : {}, } - const traces: Partial[] = [] - for (const s of Object.keys(cm) as TrialState[]) { - const bars = trials.filter((t) => t.state === s) - if (bars.length === 0) { - continue - } + const makeTrace = (bars: Trial[], state: string, color: string) => { const starts = bars.map((b) => b.datetime_start ?? new Date()) const completes = bars.map((b, i) => b.datetime_complete ?? starts[i]) const trace: Partial = { @@ -106,14 +101,38 @@ const plotTimeline = (trials: Trial[], mode: string) => { y: bars.map((b) => b.number), // @ts-ignore: To suppress ts(2322) base: starts.map((s) => s.toISOString()), - name: s, + name: state, text: bars.map((b) => makeHovertext(b)), - hovertemplate: "%{text}" + s + "", + hovertemplate: "%{text}" + state + "", orientation: "h", - marker: { color: cm[s] }, + marker: { color: color }, textposition: "none", // Avoid drawing hovertext in a bar. } - traces.push(trace) + return trace + } + + const traces: Partial[] = [] + for (const [state, color] of Object.entries(cm)) { + const bars = trials.filter((t) => t.state === state) + if (bars.length === 0) { + continue + } + if (state === "Complete") { + const feasibleTrials = bars.filter((t) => + t.constraints.every((c) => c <= 0) + ) + const infeasibleTrials = bars.filter((t) => + t.constraints.some((c) => c > 0) + ) + if (feasibleTrials.length > 0) { + traces.push(makeTrace(feasibleTrials, "Complete", color)) + } + if (infeasibleTrials.length > 0) { + traces.push(makeTrace(infeasibleTrials, "Infeasible", "#cccccc")) + } + } else { + traces.push(makeTrace(bars, state, color)) + } } plotly.react(plotDomId, traces, layout) } diff --git a/optuna_dashboard/ts/components/Note.tsx b/optuna_dashboard/ts/components/Note.tsx index 09270213..586f34e0 100644 --- a/optuna_dashboard/ts/components/Note.tsx +++ b/optuna_dashboard/ts/components/Note.tsx @@ -425,7 +425,7 @@ const ArtifactUploader: FC<{ if (files === null) { return } - action.uploadArtifact(studyId, trialId, files[0]) + action.uploadTrialArtifact(studyId, trialId, files[0]) } const handleDrop: DragEventHandler = (e) => { @@ -433,7 +433,7 @@ const ArtifactUploader: FC<{ e.preventDefault() const file = e.dataTransfer.files[0] setDragOver(false) - action.uploadArtifact(studyId, trialId, file) + action.uploadTrialArtifact(studyId, trialId, file) } const handleDragOver: DragEventHandler = (e) => { diff --git a/optuna_dashboard/ts/components/PreferenceHistory.tsx b/optuna_dashboard/ts/components/PreferenceHistory.tsx index 6aa67317..b7bd04b4 100644 --- a/optuna_dashboard/ts/components/PreferenceHistory.tsx +++ b/optuna_dashboard/ts/components/PreferenceHistory.tsx @@ -147,6 +147,16 @@ const CandidateTrial: FC<{ overflow: "auto", }} > + setDetailShown(false)} + > + + false} diff --git a/optuna_dashboard/ts/components/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/StudyArtifactCards.tsx new file mode 100644 index 00000000..c6394896 --- /dev/null +++ b/optuna_dashboard/ts/components/StudyArtifactCards.tsx @@ -0,0 +1,226 @@ +import React, { + ChangeEventHandler, + DragEventHandler, + FC, + MouseEventHandler, + useRef, + useState, +} from "react" +import { + Typography, + Box, + useTheme, + IconButton, + Card, + CardContent, + CardActionArea, +} from "@mui/material" +import UploadFileIcon from "@mui/icons-material/UploadFile" +import DownloadIcon from "@mui/icons-material/Download" +import DeleteIcon from "@mui/icons-material/Delete" +import FullscreenIcon from "@mui/icons-material/Fullscreen" + +import { actionCreator } from "../action" +import { useDeleteStudyArtifactDialog } from "./DeleteArtifactDialog" +import { + useThreejsArtifactModal, + isThreejsArtifact, +} from "./ThreejsArtifactViewer" +import { ArtifactCardMedia } from "./ArtifactCardMedia" + +export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { + const theme = useTheme() + const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = + useDeleteStudyArtifactDialog() + const [openThreejsArtifactModal, renderThreejsArtifactModal] = + useThreejsArtifactModal() + + const width = "200px" + const height = "150px" + + return ( + <> + + {study.artifacts.map((artifact) => { + const urlPath = `/artifacts/${study.id}/${artifact.artifact_id}` + return ( + + + + + {artifact.filename} + + {isThreejsArtifact(artifact) ? ( + { + openThreejsArtifactModal(urlPath, artifact) + }} + > + + + ) : null} + { + openDeleteArtifactDialog(study.id, artifact) + }} + > + + + + + + + + ) + })} + + + {renderDeleteArtifactDialog()} + {renderThreejsArtifactModal()} + + ) +} + +const StudyArtifactUploader: FC<{ + study: StudyDetail + width: string + height: string +}> = ({ study, width, height }) => { + const theme = useTheme() + const [dragOver, setDragOver] = useState(false) + const action = actionCreator() + + const inputRef = useRef(null) + const handleClick: MouseEventHandler = () => { + if (!inputRef || !inputRef.current) { + return + } + inputRef.current.click() + } + + const handleOnChange: ChangeEventHandler = (e) => { + const files = e.target.files + if (files === null) { + return + } + action.uploadStudyArtifact(study.id, files[0]) + } + + const handleDragOver: DragEventHandler = (e) => { + e.stopPropagation() + e.preventDefault() + e.dataTransfer.dropEffect = "copy" + setDragOver(true) + } + + const handleDragLeave: DragEventHandler = (e) => { + e.stopPropagation() + e.preventDefault() + e.dataTransfer.dropEffect = "copy" + setDragOver(false) + } + + const handleDrop: DragEventHandler = (e) => { + e.stopPropagation() + e.preventDefault() + const files = e.dataTransfer.files + setDragOver(false) + for (let i = 0; i < files.length; i++) { + action.uploadStudyArtifact(study.id, files[i]) + } + } + + return ( + + + + + + Upload a New File + + Drag your file here or click to browse. + + + + + ) +} diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index 1a8ced33..79702f2e 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -12,6 +12,7 @@ import { import Grid2 from "@mui/material/Unstable_Grid2" import ChevronRightIcon from "@mui/icons-material/ChevronRight" import HomeIcon from "@mui/icons-material/Home" +import DownloadIcon from "@mui/icons-material/Download" import { StudyNote } from "./Note" import { actionCreator } from "../action" @@ -27,6 +28,7 @@ import { GraphParallelCoordinate } from "./GraphParallelCoordinate" import { Contour } from "./GraphContour" import { GraphSlice } from "./GraphSlice" import { GraphEdf } from "./GraphEdf" +import { GraphRank } from "./GraphRank" import { TrialList } from "./TrialList" import { StudyHistory } from "./StudyHistory" import { PreferentialTrials } from "./PreferentialTrials" @@ -121,6 +123,11 @@ export const StudyDetail: FC<{ + + + + + Empirical Distribution of the Objective Value @@ -139,16 +146,44 @@ export const StudyDetail: FC<{ ) - } else if (page === "trialTable") { - content = ( - - - - - - ) } else if (page === "trialList") { content = + } else if (page === "trialTable") { + content = ( + + + + + + + Download CSV File + + + + + + + + + + + ) } else if (page === "note" && studyDetail !== null) { content = ( ) - } else if (page == "preferenceHistory") { + } else if (page === "preferenceHistory") { content = } diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index 907acd1a..8fab513a 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -17,12 +17,15 @@ import { DataGrid, DataGridColumn } from "./DataGrid" import { GraphHyperparameterImportance } from "./GraphHyperparameterImportances" import { UserDefinedPlot } from "./UserDefinedPlot" import { BestTrialsCard } from "./BestTrialsCard" +import { StudyArtifactCards } from "./StudyArtifactCards" +import { useRecoilValue } from "recoil" import { useStudyDetailValue, useStudyDirections, useStudySummaryValue, } from "../state" import FormControlLabel from "@mui/material/FormControlLabel" +import { artifactIsAvailable } from "../state" export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { const theme = useTheme() @@ -31,6 +34,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { const studyDetail = useStudyDetailValue(studyId) const [logScale, setLogScale] = useState(false) const [includePruned, setIncludePruned] = useState(true) + const artifactEnabled = useRecoilValue(artifactIsAvailable) const handleLogScaleChange = () => { setLogScale(!logScale) @@ -104,17 +108,6 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { - {studyDetail !== null && - studyDetail.directions.length == 1 && - studyDetail.has_intermediate_values ? ( - - - - ) : null} = ({ studyId }) => { + {studyDetail !== null && + studyDetail.directions.length === 1 && + studyDetail.has_intermediate_values ? ( + + + + ) : null} + + {artifactEnabled && studyDetail !== null && ( + + + + + + Study Artifacts + + + + + + + )} ) } diff --git a/optuna_dashboard/ts/components/StudyList.tsx b/optuna_dashboard/ts/components/StudyList.tsx index c41c51cc..14eaa68b 100644 --- a/optuna_dashboard/ts/components/StudyList.tsx +++ b/optuna_dashboard/ts/components/StudyList.tsx @@ -64,8 +64,7 @@ export const StudyList: FC<{ return useMemo(() => new URLSearchParams(search), [search]) } const query = useQuery() - const initialSortBy = - query.get("studies_order_by") === "desc" ? "desc" : "asc" + const initialSortBy = query.get("studies_order_by") === "asc" ? "asc" : "desc" const [sortBy, setSortBy] = useState<"asc" | "desc">(initialSortBy) let filteredStudies = studies.filter((s) => !studyFilter(s)) diff --git a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx index 3c4e2e01..8d8c045d 100644 --- a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx @@ -4,12 +4,17 @@ import { Canvas } from "@react-three/fiber" import { GizmoHelper, GizmoViewport, OrbitControls } from "@react-three/drei" import { STLLoader } from "three/examples/jsm/loaders/STLLoader" import { Rhino3dmLoader } from "three/examples/jsm/loaders/3DMLoader" +import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader" import { PerspectiveCamera } from "three" -import { Modal, Box } from "@mui/material" +import { Modal, Box, useTheme } from "@mui/material" +import ClearIcon from "@mui/icons-material/Clear" +import IconButton from "@mui/material/IconButton" export const isThreejsArtifact = (artifact: Artifact): boolean => { return ( - artifact.filename.endsWith(".stl") || artifact.filename.endsWith(".3dm") + artifact.filename.endsWith(".stl") || + artifact.filename.endsWith(".3dm") || + artifact.filename.endsWith(".obj") ) } @@ -32,7 +37,7 @@ const CustomGizmoHelper: React.FC = () => { ) } -const calculateBoundingBox = (geometries: THREE.BufferGeometry[]) => { +const computeBoundingBox = (geometries: THREE.BufferGeometry[]) => { const boundingBox = new THREE.Box3() geometries.forEach((geometry) => { const mesh = new THREE.Mesh(geometry) @@ -45,8 +50,11 @@ export const ThreejsArtifactViewer: React.FC = ( props ) => { const [geometry, setGeometry] = useState([]) - const [modelSize, setModelSize] = useState( - new THREE.Vector3(10, 10, 10) + const [boundingBox, setBoundingBox] = useState( + new THREE.Box3( + new THREE.Vector3(-10, -10, -10), + new THREE.Vector3(10, 10, 10) + ) ) const [cameraSettings, setCameraSettings] = useState( new THREE.PerspectiveCamera() @@ -54,55 +62,54 @@ export const ThreejsArtifactViewer: React.FC = ( const handleLoadedGeometries = (geometries: THREE.BufferGeometry[]) => { setGeometry(geometries) - const boundingBox = calculateBoundingBox(geometries) + const boundingBox = computeBoundingBox(geometries) if (boundingBox !== null) { - const size = boundingBox.getSize(new THREE.Vector3()) - setModelSize(size) + setBoundingBox(boundingBox) } + return boundingBox } useEffect(() => { if ("stl" === props.filetype) { - const stlLoader = new STLLoader() - stlLoader.load(props.src, (stlGeometries: THREE.BufferGeometry) => { - if (stlGeometries) { - handleLoadedGeometries([stlGeometries]) - } - }) + loadSTL(props, handleLoadedGeometries) } else if ("3dm" === props.filetype) { - const loader = new Rhino3dmLoader() - loader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/") - loader.load(props.src, (object: THREE.Object3D) => { - const meshes = object.children as THREE.Mesh[] - const rhinoGeometries = meshes.map((mesh) => mesh.geometry) - THREE.Object3D.DEFAULT_UP.set(0, 0, 1) - if (rhinoGeometries.length > 0) { - handleLoadedGeometries(rhinoGeometries) - } - }) + loadRhino3dm(props, handleLoadedGeometries) + } else if ("obj" === props.filetype) { + loadOBJ(props, handleLoadedGeometries) } - const maxModelSize = Math.max(modelSize.x, modelSize.y, modelSize.z) - const cameraSet = new THREE.PerspectiveCamera( - modelSize - ? Math.min( - 45, - Math.atan(modelSize.y / modelSize.z) * (180 / Math.PI) * 2 - ) - : 45, - window.innerWidth / window.innerHeight - ) - cameraSet.position.set(maxModelSize * 2, maxModelSize * 2, maxModelSize * 2) - setCameraSettings(cameraSet) }, []) + useEffect(() => { + const cameraSet = new THREE.PerspectiveCamera( + 50, + window.innerWidth / window.innerHeight, + 0.1, + boundingBox.getSize(new THREE.Vector3()).length() * 100 + ) + const maxPosition = Math.max( + boundingBox.max.x, + boundingBox.max.y, + boundingBox.max.z + ) + cameraSet.position.set( + maxPosition * 1.5, + maxPosition * 1.5, + maxPosition * 1.5 + ) + const center = boundingBox.getCenter(new THREE.Vector3()) + cameraSet.lookAt(center.x, center.y, center.z) + setCameraSettings(cameraSet) + }, [boundingBox]) + return ( - + {props.hasGizmo && } {geometry.length > 0 && @@ -121,6 +128,7 @@ export const useThreejsArtifactModal = (): [ ] => { const [open, setOpen] = useState(false) const [target, setTarget] = useState<[string, Artifact | null]>(["", null]) + const theme = useTheme() const openModal = (artifactUrlPath: string, artifact: Artifact) => { setTarget([artifactUrlPath, artifact]) @@ -146,6 +154,19 @@ export const useThreejsArtifactModal = (): [ borderRadius: "15px", }} > + { + setOpen(false) + setTarget(["", null]) + }} + > + + THREE.Box3 +) { + const stlLoader = new STLLoader() + stlLoader.load(props.src, (stlGeometries: THREE.BufferGeometry) => { + if (stlGeometries) { + handleLoadedGeometries([stlGeometries]) + } + }) +} + +function loadRhino3dm( + props: ThreejsArtifactViewerProps, + handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3 +) { + const rhino3dmLoader = new Rhino3dmLoader() + rhino3dmLoader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/") + rhino3dmLoader.load(props.src, (object: THREE.Object3D) => { + const meshes = object.children as THREE.Mesh[] + const rhinoGeometries = meshes.map((mesh) => mesh.geometry) + THREE.Object3D.DEFAULT_UP.set(0, 0, 1) + if (rhinoGeometries.length > 0) { + handleLoadedGeometries(rhinoGeometries) + } + }) +} + +function loadOBJ( + props: ThreejsArtifactViewerProps, + handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3 +) { + const objLoader = new OBJLoader() + objLoader.load(props.src, (object: THREE.Object3D) => { + const meshes = object.children as THREE.Mesh[] + const objGeometries = meshes.map((mesh) => mesh.geometry) + if (objGeometries.length > 0) { + handleLoadedGeometries(objGeometries) + } + }) +} diff --git a/optuna_dashboard/ts/components/TrialArtifactCards.tsx b/optuna_dashboard/ts/components/TrialArtifactCards.tsx index 3e15f7ed..77ac2844 100644 --- a/optuna_dashboard/ts/components/TrialArtifactCards.tsx +++ b/optuna_dashboard/ts/components/TrialArtifactCards.tsx @@ -21,7 +21,7 @@ import DeleteIcon from "@mui/icons-material/Delete" import FullscreenIcon from "@mui/icons-material/Fullscreen" import { actionCreator } from "../action" -import { useDeleteArtifactDialog } from "./DeleteArtifactDialog" +import { useDeleteTrialArtifactDialog } from "./DeleteArtifactDialog" import { useThreejsArtifactModal, isThreejsArtifact, @@ -31,9 +31,12 @@ import { ArtifactCardMedia } from "./ArtifactCardMedia" export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { const theme = useTheme() const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = - useDeleteArtifactDialog() + useDeleteTrialArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() + const isArtifactModifiable = (trial: Trial) => { + return trial.state === "Running" || trial.state === "Waiting" + } const width = "200px" const height = "150px" @@ -75,11 +78,11 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { p: theme.spacing(0.5, 0), flexGrow: 1, wordWrap: "break-word", - maxWidth: `calc(100% - ${ - isThreejsArtifact(artifact) - ? theme.spacing(12) - : theme.spacing(8) - })`, + maxWidth: `calc(100% - ${theme.spacing( + 4 + + (isThreejsArtifact(artifact) ? 4 : 0) + + (isArtifactModifiable(trial) ? 4 : 0) + )})`, }} > {artifact.filename} @@ -97,21 +100,23 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { ) : null} - { - openDeleteArtifactDialog( - trial.study_id, - trial.trial_id, - artifact - ) - }} - > - - + {isArtifactModifiable(trial) ? ( + { + openDeleteArtifactDialog( + trial.study_id, + trial.trial_id, + artifact + ) + }} + > + + + ) : null} = ({ trial }) => { ) })} - + {isArtifactModifiable(trial) ? ( + + ) : null} {renderDeleteArtifactDialog()} {renderThreejsArtifactModal()} @@ -143,9 +150,6 @@ const TrialArtifactUploader: FC<{ const action = actionCreator() const [dragOver, setDragOver] = useState(false) - if (trial.state !== "Running" && trial.state !== "Waiting") { - return null - } const inputRef = useRef(null) const handleClick: MouseEventHandler = () => { if (!inputRef || !inputRef.current) { @@ -158,7 +162,7 @@ const TrialArtifactUploader: FC<{ if (files === null) { return } - action.uploadArtifact(trial.study_id, trial.trial_id, files[0]) + action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[0]) } const handleDrop: DragEventHandler = (e) => { e.stopPropagation() @@ -166,7 +170,7 @@ const TrialArtifactUploader: FC<{ const files = e.dataTransfer.files setDragOver(false) for (let i = 0; i < files.length; i++) { - action.uploadArtifact(trial.study_id, trial.trial_id, files[i]) + action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[i]) } } const handleDragOver: DragEventHandler = (e) => { diff --git a/optuna_dashboard/ts/components/TrialFormWidgets.tsx b/optuna_dashboard/ts/components/TrialFormWidgets.tsx index 48d5094f..f727ac80 100644 --- a/optuna_dashboard/ts/components/TrialFormWidgets.tsx +++ b/optuna_dashboard/ts/components/TrialFormWidgets.tsx @@ -45,12 +45,12 @@ export const TrialFormWidgets: FC<{ ? "Set Objective Values Form" : "Set Objective Value Form" const widgetNames = formWidgets.widgets.map((widget, i) => { - if (formWidgets.output_type == "objective") { + if (formWidgets.output_type === "objective") { if (objectiveNames.at(i) !== undefined) { return objectiveNames[i] } - return directions.length == 1 ? "Objective" : `Objective ${i}` - } else if (formWidgets.output_type == "user_attr") { + return directions.length === 1 ? "Objective" : `Objective ${i}` + } else if (formWidgets.output_type === "user_attr") { if (widget.type !== "user_attr" && widget.user_attr_key !== undefined) { return widget.user_attr_key } @@ -118,13 +118,13 @@ const UpdatableFormWidgets: FC<{ const handleSubmit = (e: React.MouseEvent): void => { e.preventDefault() const values = widgetStates.map((ws) => ws.value) - if (formWidgets.output_type == "objective") { + if (formWidgets.output_type === "objective") { const filtered = values.filter((v): v is number => v !== null) if (filtered.length !== formWidgets.widgets.length) { return } action.makeTrialComplete(trial.study_id, trial.trial_id, filtered) - } else if (formWidgets.output_type == "user_attr") { + } else if (formWidgets.output_type === "user_attr") { const user_attrs = Object.fromEntries( formWidgets.widgets.map((widget, i) => [ widget.user_attr_key, @@ -433,7 +433,7 @@ const ReadonlyFormWidgets: FC<{ max={widget.max} step={widget.step} marks={ - widget.labels === null || widget.labels.length == 0 + widget.labels === null || widget.labels.length === 0 ? true : widget.labels } diff --git a/optuna_dashboard/ts/components/TrialList.tsx b/optuna_dashboard/ts/components/TrialList.tsx index 6922003a..cfde6536 100644 --- a/optuna_dashboard/ts/components/TrialList.tsx +++ b/optuna_dashboard/ts/components/TrialList.tsx @@ -119,7 +119,7 @@ const useIsBestTrial = ( return useMemo(() => { const bestTrialIDs = studyDetail?.best_trials.map((t) => t.trial_id) || [] return (trialId: number): boolean => - bestTrialIDs.findIndex((a) => a === trialId) != -1 + bestTrialIDs.findIndex((a) => a === trialId) !== -1 }, [studyDetail]) } diff --git a/optuna_dashboard/ts/components/TrialTable.tsx b/optuna_dashboard/ts/components/TrialTable.tsx index 8fcce63c..efc79626 100644 --- a/optuna_dashboard/ts/components/TrialTable.tsx +++ b/optuna_dashboard/ts/components/TrialTable.tsx @@ -18,17 +18,17 @@ export const TrialTable: FC<{ field: "state", label: "State", sortable: true, - filterable: true, + filterChoices: ["Complete", "Pruned", "Fail", "Running", "Waiting"], padding: "none", toCellValue: (i) => trials[i].state.toString(), }, ] - if (studyDetail === null || studyDetail.directions.length == 1) { + if (studyDetail === null || studyDetail.directions.length === 1) { columns.push({ field: "values", label: "Value", sortable: true, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, ascending): number => { const firstVal = firstEl.values?.[0] const secondVal = secondEl.values?.[0] @@ -36,9 +36,9 @@ export const TrialTable: FC<{ return 0 } if (firstVal === undefined) { - return -1 + return ascending ? -1 : 1 } else if (secondVal === undefined) { - return 1 + return ascending ? 1 : -1 } if (firstVal === "-inf" || secondVal === "inf") { return 1 @@ -63,7 +63,7 @@ export const TrialTable: FC<{ ? objectiveNames[objectiveId] : `Objective ${objectiveId}`, sortable: true, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, ascending): number => { const firstVal = firstEl.values?.[objectiveId] const secondVal = secondEl.values?.[objectiveId] @@ -71,9 +71,9 @@ export const TrialTable: FC<{ return 0 } if (firstVal === undefined) { - return -1 + return ascending ? -1 : 1 } else if (secondVal === undefined) { - return 1 + return ascending ? 1 : -1 } if (firstVal === "-inf" || secondVal === "inf") { return 1 @@ -97,7 +97,10 @@ export const TrialTable: FC<{ ) { studyDetail?.intersection_search_space.forEach((s) => { const sortable = s.distribution.type !== "CategoricalDistribution" - const filterable = s.distribution.type === "CategoricalDistribution" + const filterChoices = + s.distribution.type === "CategoricalDistribution" + ? s.distribution.choices.map((c) => c.value) + : undefined columns.push({ field: "params", label: `Param ${s.name}`, @@ -105,8 +108,9 @@ export const TrialTable: FC<{ trials[i].params.find((p) => p.name === s.name) ?.param_external_value || null, sortable: sortable, - filterable: filterable, - less: (firstEl, secondEl): number => { + filterChoices: filterChoices, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + less: (firstEl, secondEl, _): number => { const firstVal = firstEl.params.find( (p) => p.name === s.name )?.param_internal_value @@ -145,8 +149,8 @@ export const TrialTable: FC<{ trials[i].user_attrs.find((attr) => attr.key === attr_spec.key) ?.value || null, sortable: attr_spec.sortable, - filterable: !attr_spec.sortable, - less: (firstEl, secondEl): number => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + less: (firstEl, secondEl, _): number => { const firstVal = firstEl.user_attrs.find( (attr) => attr.key === attr_spec.key )?.value diff --git a/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx new file mode 100644 index 00000000..74177602 --- /dev/null +++ b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx @@ -0,0 +1,72 @@ +import React, { useCallback, useEffect, useState, useRef } from "react" +import WaveSurfer from "wavesurfer.js" +import { Box } from "@mui/material" + +interface WaveSurferArtifactViewerProps { + height: number + waveColor: string + progressColor: string + url: string +} + +const useWavesurfer = ( + containerRef: React.MutableRefObject, + options: WaveSurferArtifactViewerProps +) => { + const [wavesurfer, setWavesurfer] = useState(null) + + useEffect(() => { + if (!containerRef.current) return + + const ws = WaveSurfer.create({ + ...options, + container: containerRef.current, + }) + + setWavesurfer(ws) + + return () => { + ws.destroy() + } + }, [containerRef]) + + return wavesurfer +} + +// Create a React component of wavesurfer. +export const WaveSurferArtifactViewer: React.FC< + WaveSurferArtifactViewerProps +> = (props) => { + const containerRef = useRef(null!) + const [isPlaying, setIsPlaying] = useState(false) + const wavesurfer = useWavesurfer(containerRef, props) + + const onPlayClick = useCallback(() => { + if (!wavesurfer) return + wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play() + }, [wavesurfer]) + + useEffect(() => { + if (!wavesurfer) return + + setIsPlaying(false) + + const subscriptions = [ + wavesurfer.on("play", () => setIsPlaying(true)), + wavesurfer.on("pause", () => setIsPlaying(false)), + ] + + return () => { + subscriptions.forEach((unsub) => unsub()) + } + }, [wavesurfer]) + + return ( + +
+ + + ) +} diff --git a/optuna_dashboard/ts/dominatedTrials.ts b/optuna_dashboard/ts/dominatedTrials.ts index 06afb15f..b1571775 100644 --- a/optuna_dashboard/ts/dominatedTrials.ts +++ b/optuna_dashboard/ts/dominatedTrials.ts @@ -27,7 +27,7 @@ export const getDominatedTrials = ( const dominatedTrials: boolean[] = [] normalizedValues.forEach((values0: number[], i: number) => { const dominated = normalizedValues.some((values1: number[], j: number) => { - if (i === j || values0.every((v, i) => v == values1[i])) { + if (i === j || values0.every((v, i) => v === values1[i])) { return false } return values0.every((value0: number, k: number) => { diff --git a/optuna_dashboard/ts/graphUtil.ts b/optuna_dashboard/ts/graphUtil.ts index 1b1c69cb..35a9002b 100644 --- a/optuna_dashboard/ts/graphUtil.ts +++ b/optuna_dashboard/ts/graphUtil.ts @@ -1,3 +1,104 @@ +const PADDING_RATIO = 0.05 + +export type AxisInfo = { + name: string + isLog: boolean + isCat: boolean + indices: (string | number)[] + values: (string | number | null)[] +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const unique = (array: any[]) => { + const knownElements = new Map() + array.forEach((elem) => knownElements.set(elem, true)) + return Array.from(knownElements.keys()) +} + +export const getAxisInfo = ( + trials: Trial[], + param: SearchSpaceItem +): AxisInfo => { + if (param.distribution.type === "CategoricalDistribution") { + return getAxisInfoForCategoricalParams( + trials, + param.name, + param.distribution + ) + } else { + return getAxisInfoForNumericalParams(trials, param.name, param.distribution) + } +} + +const getAxisInfoForCategoricalParams = ( + trials: Trial[], + paramName: string, + distribution: CategoricalDistribution +): AxisInfo => { + const values = trials.map( + (trial) => + trial.params.find((p) => p.name === paramName)?.param_external_value || + null + ) + + const indices = distribution.choices + .map((c) => c.value) + .sort((a, b) => + a.toLowerCase() < b.toLowerCase() + ? -1 + : a.toLowerCase() > b.toLowerCase() + ? 1 + : 0 + ) + return { + name: paramName, + isLog: false, + isCat: true, + indices, + values, + } +} + +const getAxisInfoForNumericalParams = ( + trials: Trial[], + paramName: string, + distribution: FloatDistribution | IntDistribution +): AxisInfo => { + let min = 0 + let max = 0 + if (distribution.log) { + const padding = + (Math.log10(distribution.high) - Math.log10(distribution.low)) * + PADDING_RATIO + min = Math.pow(10, Math.log10(distribution.low) - padding) + max = Math.pow(10, Math.log10(distribution.high) + padding) + } else { + const padding = (distribution.high - distribution.low) * PADDING_RATIO + min = distribution.low - padding + max = distribution.high + padding + } + + const values = trials.map( + (trial) => + trial.params.find((p) => p.name === paramName)?.param_internal_value || + null + ) + const indices = unique(values) + .filter((v) => v !== null) + .sort((a, b) => a - b) + if (indices.length >= 2) { + indices.unshift(min) + indices.push(max) + } + return { + name: paramName, + isLog: distribution.log, + isCat: false, + indices, + values, + } +} + export const makeHovertext = (trial: Trial): string => { return JSON.stringify( { diff --git a/optuna_dashboard/ts/state.ts b/optuna_dashboard/ts/state.ts index 3c6f0fd7..94e8d2d7 100644 --- a/optuna_dashboard/ts/state.ts +++ b/optuna_dashboard/ts/state.ts @@ -63,7 +63,7 @@ export const useStudyDetailValue = (studyId: number): StudyDetail | null => { export const useStudySummaryValue = (studyId: number): StudySummary | null => { const studySummaries = useRecoilValue(studySummariesState) - return studySummaries.find((s) => s.study_id == studyId) || null + return studySummaries.find((s) => s.study_id === studyId) || null } export const useTrialUpdatingValue = (trialId: number): boolean => { diff --git a/package-lock.json b/package-lock.json index a3f13d65..05c79bb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@react-three/drei": "^9.80.0", "@react-three/fiber": "^8.13.6", "@types/three": "^0.154.0", - "axios": "^1.2.1", + "axios": "^1.6.0", "elkjs": "^0.8.2", "notistack": "^3.0.1", "plotly.js-dist-min": "^2.22.0", @@ -32,7 +32,8 @@ "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", - "three": "^0.155.0" + "three": "^0.155.0", + "wavesurfer.js": "^7.4.12" }, "devDependencies": { "@babel/core": "^7.14.3", @@ -43,12 +44,12 @@ "@types/react": "^18.0.26", "@types/react-dom": "^18.0.10", "@types/react-syntax-highlighter": "^15.5.5", - "@typescript-eslint/eslint-plugin": "^4.26.1", - "@typescript-eslint/parser": "^4.26.1", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", "compression-webpack-plugin": "^10.0.0", "css-loader": "^6.8.1", "esbuild-loader": "^2.18.0", - "eslint": "^7.28.0", + "eslint": "^8.53.0", "jest": "^29.2.1", "jest-canvas-mock": "^2.3.1", "jest-environment-jsdom": "^29.3.1", @@ -61,6 +62,15 @@ "webpack-cli": "^4.9.2" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -75,11 +85,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -125,13 +136,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", - "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.20.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -250,9 +262,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" @@ -271,25 +283,25 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -417,29 +429,29 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -483,12 +495,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -496,9 +508,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1657,33 +1669,33 @@ } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "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" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", - "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "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", + "@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" }, @@ -1692,12 +1704,12 @@ } }, "node_modules/@babel/types": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", - "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1932,30 +1944,63 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "dependencies": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1967,13 +2012,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "engines": { - "node": ">= 4" + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { @@ -1988,24 +2036,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/js": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -4274,9 +4344,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/katex": { @@ -4407,6 +4477,12 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "node_modules/@types/semver": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", + "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -4468,30 +4544,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", - "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz", + "integrity": "sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==", "dev": true, "dependencies": { - "@typescript-eslint/experimental-utils": "4.33.0", - "@typescript-eslint/scope-manager": "4.33.0", - "debug": "^4.3.1", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.1.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/type-utils": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^4.0.0", - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4500,9 +4579,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4514,50 +4593,27 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", - "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", - "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.10.0.tgz", + "integrity": "sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "debug": "^4.3.1" + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4566,29 +4622,56 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", - "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz", + "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0" }, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz", + "integrity": "sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/types": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", - "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", + "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", "dev": true, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4596,21 +4679,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", - "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz", + "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0", - "debug": "^4.3.1", - "globby": "^11.0.3", - "is-glob": "^4.0.1", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4623,9 +4706,49 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.10.0.tgz", + "integrity": "sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4638,22 +4761,28 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", - "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", + "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.33.0", - "eslint-visitor-keys": "^2.0.0" + "@typescript-eslint/types": "6.10.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@use-gesture/core": { "version": "10.2.27", "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.27.tgz", @@ -4981,15 +5110,6 @@ "ajv": "^6.9.1" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -5065,15 +5185,6 @@ "node": ">=8" } }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5092,9 +5203,9 @@ } }, "node_modules/axios": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", - "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -6081,18 +6192,6 @@ "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", @@ -6621,57 +6720,55 @@ } }, "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "dev": true, "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6690,40 +6787,16 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint/node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ansi-styles": { @@ -6741,6 +6814,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6787,34 +6866,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { - "node": ">=4" + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -6835,28 +6931,61 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" + "argparse": "^2.0.1" }, "bin": { - "semver": "bin/semver.js" + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/supports-color": { @@ -6892,26 +7021,32 @@ } }, "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/espree/node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=4" + "node": ">=0.4.0" } }, "node_modules/esprima": { @@ -6927,9 +7062,9 @@ } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -7054,9 +7189,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7069,6 +7204,18 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7090,9 +7237,9 @@ } }, "node_modules/fastq": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", - "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -7167,22 +7314,23 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "node_modules/follow-redirects": { @@ -7259,12 +7407,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -7348,15 +7490,15 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/glob-to-regexp": { @@ -7425,6 +7567,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/hamt_plus": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", @@ -7819,9 +7967,9 @@ } }, "node_modules/ignore": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", - "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true, "engines": { "node": ">= 4" @@ -8121,6 +8269,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -10937,6 +11094,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -10966,6 +11129,15 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -11094,12 +11266,6 @@ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==" }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -12315,17 +12481,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -12511,9 +12677,9 @@ "integrity": "sha512-2b7w4CQI06px8HVpKpgZtfuoDjuCLA26VlgdnG71UDBrJvtCYvXb39H4ElNv+CA1bbD4S98KanpWPRqTqlxBZw==" }, "node_modules/postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -12685,15 +12851,6 @@ "node": ">=6" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -13080,18 +13237,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/regexpu-core": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", @@ -13626,56 +13771,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -13915,44 +14010,6 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, - "node_modules/table": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", - "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -14199,6 +14256,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -14361,27 +14430,6 @@ "node": ">=8" } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14679,12 +14727,6 @@ "node": ">=6" } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "node_modules/v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -14783,6 +14825,11 @@ "node": ">=10.13.0" } }, + "node_modules/wavesurfer.js": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.4.12.tgz", + "integrity": "sha512-KzH4LkcOp8LECs9cOVIPBl6vsSoICKuZz+v5kh/zvxilpaVszU+QKC+4s2KEAqcCxBCecg3cNSg4RqAx278F8g==" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -15302,6 +15349,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -15313,11 +15366,12 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -15350,13 +15404,14 @@ } }, "@babel/generator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", - "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "requires": { - "@babel/types": "^7.20.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -15444,9 +15499,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-explode-assignable-expression": { @@ -15459,22 +15514,22 @@ } }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -15569,23 +15624,23 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -15617,19 +15672,19 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -16401,41 +16456,41 @@ } }, "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@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": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", - "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "requires": { - "@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", + "@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": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", - "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -16620,37 +16675,61 @@ "dev": true, "optional": true }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true + }, "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "requires": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } }, "type-fest": { "version": "0.20.2", @@ -16660,21 +16739,33 @@ } } }, + "@eslint/js": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "dev": true + }, "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.0", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" } }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "@istanbuljs/load-nyc-config": { @@ -18329,9 +18420,9 @@ } }, "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "@types/katex": { @@ -18464,6 +18555,12 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "@types/semver": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", + "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -18525,25 +18622,28 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", - "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz", + "integrity": "sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.33.0", - "@typescript-eslint/scope-manager": "4.33.0", - "debug": "^4.3.1", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.1.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/type-utils": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -18551,67 +18651,92 @@ } } }, - "@typescript-eslint/experimental-utils": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", - "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } - }, "@typescript-eslint/parser": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", - "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.10.0.tgz", + "integrity": "sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "debug": "^4.3.1" + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", - "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz", + "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz", + "integrity": "sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", - "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", + "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", - "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz", + "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0", - "debug": "^4.3.1", - "globby": "^11.0.3", - "is-glob": "^4.0.1", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.10.0.tgz", + "integrity": "sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -18620,15 +18745,21 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", - "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", + "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "eslint-visitor-keys": "^2.0.0" + "@typescript-eslint/types": "6.10.0", + "eslint-visitor-keys": "^3.4.1" } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "@use-gesture/core": { "version": "10.2.27", "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.27.tgz", @@ -18910,12 +19041,6 @@ "dev": true, "requires": {} }, - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true - }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -18973,12 +19098,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -18991,9 +19110,9 @@ "dev": true }, "axios": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", - "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -19729,15 +19848,6 @@ "tapable": "^2.2.0" } }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, "entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", @@ -20030,62 +20140,51 @@ } }, "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "dev": true, "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -20095,6 +20194,12 @@ "color-convert": "^2.0.1" } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -20126,27 +20231,36 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" } }, "globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -20158,19 +20272,40 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "requires": { - "lru-cache": "^6.0.0" + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" } }, "supports-color": { @@ -20200,19 +20335,10 @@ "estraverse": "^4.1.1" } }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - } - }, "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "esm": { @@ -20221,20 +20347,20 @@ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" }, "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true } } @@ -20245,9 +20371,9 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -20343,9 +20469,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -20353,6 +20479,17 @@ "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } } }, "fast-json-stable-stringify": { @@ -20373,9 +20510,9 @@ "dev": true }, "fastq": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", - "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -20437,19 +20574,20 @@ } }, "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "requires": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "follow-redirects": { @@ -20499,12 +20637,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -20561,12 +20693,12 @@ } }, "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" } }, "glob-to-regexp": { @@ -20621,6 +20753,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "hamt_plus": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", @@ -20908,9 +21046,9 @@ "requires": {} }, "ignore": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", - "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, "import-fresh": { @@ -21099,6 +21237,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, "is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -23205,6 +23349,12 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -23228,6 +23378,15 @@ "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "dev": true }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -23335,12 +23494,6 @@ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==" }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true - }, "longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -24136,17 +24289,17 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "p-limit": { @@ -24278,9 +24431,9 @@ "integrity": "sha512-2b7w4CQI06px8HVpKpgZtfuoDjuCLA26VlgdnG71UDBrJvtCYvXb39H4ElNv+CA1bbD4S98KanpWPRqTqlxBZw==" }, "postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { "nanoid": "^3.3.6", @@ -24387,12 +24540,6 @@ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==" }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -24676,12 +24823,6 @@ "functions-have-names": "^1.2.2" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, "regexpu-core": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", @@ -25068,43 +25209,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -25285,39 +25389,6 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, - "table": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", - "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", - "dev": true, - "requires": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -25492,6 +25563,13 @@ "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==" }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "requires": {} + }, "ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -25591,21 +25669,6 @@ } } }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -25805,12 +25868,6 @@ } } }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -25887,6 +25944,11 @@ "graceful-fs": "^4.1.2" } }, + "wavesurfer.js": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.4.12.tgz", + "integrity": "sha512-KzH4LkcOp8LECs9cOVIPBl6vsSoICKuZz+v5kh/zvxilpaVszU+QKC+4s2KEAqcCxBCecg3cNSg4RqAx278F8g==" + }, "web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index a675a1ce..9e6e59e9 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@react-three/drei": "^9.80.0", "@react-three/fiber": "^8.13.6", "@types/three": "^0.154.0", - "axios": "^1.2.1", + "axios": "^1.6.0", "elkjs": "^0.8.2", "notistack": "^3.0.1", "plotly.js-dist-min": "^2.22.0", @@ -41,7 +41,8 @@ "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", - "three": "^0.155.0" + "three": "^0.155.0", + "wavesurfer.js": "^7.4.12" }, "devDependencies": { "@babel/core": "^7.14.3", @@ -52,12 +53,12 @@ "@types/react": "^18.0.26", "@types/react-dom": "^18.0.10", "@types/react-syntax-highlighter": "^15.5.5", - "@typescript-eslint/eslint-plugin": "^4.26.1", - "@typescript-eslint/parser": "^4.26.1", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", "compression-webpack-plugin": "^10.0.0", "css-loader": "^6.8.1", "esbuild-loader": "^2.18.0", - "eslint": "^7.28.0", + "eslint": "^8.53.0", "jest": "^29.2.1", "jest-canvas-mock": "^2.3.1", "jest-environment-jsdom": "^29.3.1", diff --git a/pyproject.toml b/pyproject.toml index 4eff6f1f..1f125735 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dynamic = ["version"] [project.optional-dependencies] docs = [ "boto3", + "botorch", "streamlit", "sphinx", "sphinx_rtd_theme", @@ -51,9 +52,12 @@ test = [ optional = [ "streamlit", "boto3", - "botorch", + "botorch>=0.8.1; python_version>='3.8'", ] +preferential = [ + "botorch>=0.8.1", +] [project.scripts] optuna-dashboard = "optuna_dashboard._cli:main" diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 74698a09..52fe6027 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -1,9 +1,18 @@ +import base64 +import json +import tempfile from unittest.mock import MagicMock +import optuna +from optuna.artifacts import FileSystemArtifactStore +from optuna.artifacts import upload_artifact from optuna.storages import BaseStorage +from optuna_dashboard._app import create_app from optuna_dashboard.artifact import _backend import pytest +from ..wsgi_client import send_request + def test_get_artifact_path() -> None: study = MagicMock(_study_id=0) @@ -12,7 +21,7 @@ def test_get_artifact_path() -> None: def test_artifact_prefix() -> None: - actual = _backend._dashboard_trial_artifact_prefix(trial_id=0) + actual = _backend._dashboard_artifact_prefix(trial_id=0) assert actual == "dashboard:artifacts:0:" @@ -80,3 +89,163 @@ def test_list_trial_artifacts(init_storage_with_artifact_meta: MagicMock) -> Non {"artifact_id": "id1", "filename": "bar.txt"}, {"artifact_id": "id2", "filename": "baz.txt"}, ] + + +def test_study_artifact_store_none() -> None: + storage = optuna.storages.InMemoryStorage() + app = create_app(storage) + status, _, body = send_request( + app, + "/artifacts/0/0", + "GET", + ) + assert status == 400 + + +def test_study_artifact_not_found() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + app = create_app(storage, artifact_store) + status, _, body = send_request( + app, + f"/artifacts/{study._study_id}/abc123", + "GET", + ) + assert status == 404 + + +def test_successful_study_artifact_retrieval() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + with tempfile.NamedTemporaryFile() as f: + f.write(b"dummy_content") + f.flush() + artifact_id = upload_artifact(study, f.name, artifact_store=artifact_store) + app = create_app(storage, artifact_store) + status, _, body = send_request( + app, + f"/artifacts/{study._study_id}/{artifact_id}", + "GET", + ) + assert status == 200 + assert body == b"dummy_content" + + +def test_trial_artifact_store_none() -> None: + storage = optuna.storages.InMemoryStorage() + app = create_app(storage) + status, _, body = send_request( + app, + "/artifacts/0/0/0", + "GET", + ) + assert status == 400 + + +def test_trial_artifact_not_found() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + trial = study.ask() + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + app = create_app(storage, artifact_store) + status, _, body = send_request( + app, + f"/artifacts/{study._study_id}/{trial._trial_id}/abc123", + "GET", + ) + assert status == 404 + + +def test_successful_trial_artifact_retrieval() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + trial = study.ask() + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + with tempfile.NamedTemporaryFile() as f: + f.write(b"dummy_content") + f.flush() + artifact_id = upload_artifact(trial, f.name, artifact_store=artifact_store) + app = create_app(storage, artifact_store) + status, _, body = send_request( + app, + f"/artifacts/{study._study_id}/{trial._trial_id}/{artifact_id}", + "GET", + ) + assert status == 200 + assert body == b"dummy_content" + + +DUMMY_DATA_URL = ( + f"data:text/plain; charset=utf-8,{base64.b64encode(b'dummy_content').decode('utf-8')}" +) + + +def test_upload_artifact_invalid_no_trial() -> None: + storage = optuna.storages.InMemoryStorage() + + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + + app = create_app(storage, artifact_store) + study = optuna.create_study(storage=storage) + + status, _, body = send_request( + app, + f"/api/artifacts/{study._study_id}/0", + "POST", + body=json.dumps({"file": DUMMY_DATA_URL}), + content_type="application/json", + ) + assert status == 500 # TODO(contramundum53): This should return 400 + + +def test_upload_artifact_invalid_complete_trial() -> None: + storage = optuna.storages.InMemoryStorage() + + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + + app = create_app(storage, artifact_store) + study = optuna.create_study(storage=storage) + + study.add_trial(optuna.create_trial(value=1.0, distributions={}, params={})) + trial = study.trials[-1] + status, _, body = send_request( + app, + f"/api/artifacts/{study._study_id}/{trial._trial_id}", + "POST", + body=json.dumps({"file": DUMMY_DATA_URL}), + content_type="application/json", + ) + assert status == 400 + + +def test_upload_artifact() -> None: + storage = optuna.storages.InMemoryStorage() + + study = optuna.create_study(storage=storage) + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + + app = create_app(storage, artifact_store) + + study.add_trial(optuna.create_trial(state=optuna.trial.TrialState.RUNNING)) + trial = study.trials[-1] + status, _, body = send_request( + app, + f"/api/artifacts/{study._study_id}/{trial._trial_id}", + "POST", + body=json.dumps({"file": DUMMY_DATA_URL}), + content_type="application/json", + ) + assert status == 201 + res = json.loads(body) + with open(f"{tmpdir}/{res['artifact_id']}", "r") as f: + data = f.read() + assert data == "dummy_content" diff --git a/python_tests/preferential/samplers/test_gp.py b/python_tests/preferential/samplers/test_gp.py index 08acca46..2ba53ce5 100644 --- a/python_tests/preferential/samplers/test_gp.py +++ b/python_tests/preferential/samplers/test_gp.py @@ -3,12 +3,12 @@ from unittest.mock import patch import numpy as np import pytest -import torch if sys.version_info >= (3, 8): from optuna_dashboard.preferential.samplers.gp import _one_side_trunc_norm_sampling from optuna_dashboard.preferential.samplers.gp import _orthants_MVN_Gibbs_sampling + import torch else: pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True) @@ -36,3 +36,19 @@ def test_one_side_trunc_norm_sampling() -> None: assert np.allclose( _one_side_trunc_norm_sampling(torch.Tensor([5])).numpy(), 5.426934003050024 ) + +def test_one_side_trunc_norm_sampling() -> None: + for lower in np.linspace(-10, 10, 100): + assert _one_side_trunc_norm_sampling(torch.tensor([lower], dtype=torch.float64)) >= lower + + with patch.object(torch, "rand", return_value=torch.tensor([0.4], dtype=torch.float64)): + sampled_value = _one_side_trunc_norm_sampling(torch.tensor([0.1], dtype=torch.float64)) + assert np.allclose(sampled_value.numpy(), 0.899967154837563) + + with patch.object(torch, "rand", return_value=torch.tensor([0.8], dtype=torch.float64)): + sampled_value = _one_side_trunc_norm_sampling(torch.tensor([-2.3], dtype=torch.float64)) + assert np.allclose(sampled_value.numpy(), -0.8113606739551955) + + with patch.object(torch, "rand", return_value=torch.tensor([0.1], dtype=torch.float64)): + sampled_value = _one_side_trunc_norm_sampling(torch.tensor([5], dtype=torch.float64)) + assert np.allclose(sampled_value.numpy(), 5.426934003050024) diff --git a/python_tests/preferential/test_study.py b/python_tests/preferential/test_study.py index 7fc1aaba..9b4a3f31 100644 --- a/python_tests/preferential/test_study.py +++ b/python_tests/preferential/test_study.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy import multiprocessing import pickle +import sys from typing import Callable from unittest.mock import patch import uuid @@ -22,6 +23,10 @@ from ..storage_supplier import parametrize_storages from ..storage_supplier import StorageSupplier +if sys.version_info < (3, 8): + pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True) + + @parametrize_storages def test_study_set_and_get_user_attrs(storage_supplier: Callable[[], StorageSupplier]) -> None: with storage_supplier() as storage: diff --git a/python_tests/test_api.py b/python_tests/test_api.py index f9c56e4f..e7210710 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import sys from unittest import TestCase import optuna @@ -8,12 +9,15 @@ from optuna import get_all_study_summaries from optuna.study import StudyDirection from optuna_dashboard._app import create_app from optuna_dashboard._app import create_new_study +from optuna_dashboard._note import note_str_key_prefix +from optuna_dashboard._note import note_ver_key from optuna_dashboard._preference_setting import register_preference_feedback_component from optuna_dashboard._preferential_history import NewHistory from optuna_dashboard._preferential_history import remove_history from optuna_dashboard._preferential_history import report_history from optuna_dashboard._serializer import serialize_preference_history from optuna_dashboard.preferential import create_study +import pytest from .wsgi_client import send_request @@ -105,6 +109,7 @@ class APITestCase(TestCase): ) self.assertEqual(status, 400) + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_get_best_trials_of_preferential_study(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(n_generate=4, storage=storage) @@ -128,6 +133,7 @@ class APITestCase(TestCase): assert len(best_trials) == 1 assert best_trials[0]["number"] == 0 + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_report_preference(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(n_generate=4, storage=storage) @@ -161,6 +167,7 @@ class APITestCase(TestCase): assert better.number == 2 assert worse.number == 1 + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_report_preference_when_typo_mode(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(storage=storage, n_generate=3) @@ -184,6 +191,7 @@ class APITestCase(TestCase): ) self.assertEqual(status, 400) + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_change_component(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(storage=storage, n_generate=3) @@ -214,6 +222,111 @@ class APITestCase(TestCase): assert study_detail["feedback_component_type"]["output_type"] == "artifact" assert study_detail["feedback_component_type"]["artifact_key"] == "image" + def test_save_trial_user_attrs(self) -> None: + study = optuna.create_study() + trials: list[optuna.Trial] = [] + for _ in range(2): + trial = study.ask() + trials.append(trial) + + request_body = { + "user_attrs": { + "number": 0, + }, + } + + app = create_app(study._storage) + status, _, _ = send_request( + app, + f"/api/trials/{trials[0]._trial_id}/user-attrs", + "POST", + content_type="application/json", + body=json.dumps(request_body), + ) + self.assertEqual(status, 204) + + assert study.trials[0].user_attrs == request_body["user_attrs"] + assert study.trials[1].user_attrs == {} + + def test_save_trial_user_attrs_empty(self) -> None: + study = optuna.create_study() + trial = study.ask() + + app = create_app(study._storage) + status, _, _ = send_request( + app, + f"/api/trials/{trial._trial_id}/user-attrs", + "POST", + content_type="application/json", + body=json.dumps({}), + ) + self.assertEqual(status, 400) + assert study.trials[0].user_attrs == {} + + def _save_trial_note(self, request_body: dict[str, int | str]) -> tuple[int, optuna.Study]: + study = optuna.create_study() + trial = study.ask() + app = create_app(study._storage) + status, _, _ = send_request( + app, + f"/api/studies/{study._study_id}/{trial._trial_id}/note", + "PUT", + content_type="application/json", + body=json.dumps(request_body), + ) + return status, study + + def test_save_trial_note_overwrite(self) -> None: + study = optuna.create_study() + trial = study.ask() + app = create_app(study._storage) + + def _get_request_body(note_version: int) -> dict[str, str | int]: + return {"body": f"Test note ver. {note_version}.", "version": note_version} + + for ver in range(1, 3): + status, _, _ = send_request( + app, + f"/api/studies/{study._study_id}/{trial._trial_id}/note", + "PUT", + content_type="application/json", + body=json.dumps(_get_request_body(note_version=ver)), + ) + assert status == 204 + # Check if the version 1 is deleted. + expected_request_body = _get_request_body(note_version=2) + expected_system_attrs = { + note_ver_key(trial_id=0): expected_request_body["version"], + f"{note_str_key_prefix(trial_id=0)}{0}": expected_request_body["body"], + } + for k, v in expected_system_attrs.items(): + assert k in study.system_attrs + assert study.system_attrs[k] == v + + def test_save_trial_note(self) -> None: + request_body: dict[str, int | str] = {"body": "Test note.", "version": 1} + status, study = self._save_trial_note(request_body) + assert status == 204 + expected_system_attrs = { + note_ver_key(0): request_body["version"], + f"{note_str_key_prefix(0)}{0}": request_body["body"], + } + for k, v in expected_system_attrs.items(): + assert k in study.system_attrs + assert study.system_attrs[k] == v + + def test_save_trial_note_with_wrong_version(self) -> None: + request_body: dict[str, int | str] = {"body": "Test note.", "version": 0} + status, study = self._save_trial_note(request_body) + assert status == 409 + assert note_ver_key(0) not in study.system_attrs + + def test_save_trial_note_empty(self) -> None: + status, study = self._save_trial_note(request_body={}) + assert status == 400 + assert note_ver_key(0) not in study.system_attrs + + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_skip_trial(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(n_generate=4, storage=storage) @@ -238,6 +351,7 @@ class APITestCase(TestCase): assert len(best_trials) == 1 assert best_trials[0].number == 2 + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_remove_history(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(storage=storage, n_generate=3) @@ -271,6 +385,7 @@ class APITestCase(TestCase): assert histories[0]["is_removed"] assert len(study.get_preferences()) == 0 + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_restore_history(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(storage=storage, n_generate=3) @@ -390,6 +505,125 @@ class APITestCase(TestCase): ) self.assertEqual(status, 404) + def test_tell_trial_complete(self) -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + trial_id = study.ask()._trial_id + + app = create_app(storage) + status, _, _ = send_request( + app, + f"/api/trials/{trial_id}/tell", + "POST", + body=json.dumps( + { + "state": "Complete", + "values": [0, 1, 2], + } + ), + content_type="application/json", + ) + self.assertEqual(status, 204) + trial = storage.get_trial(trial_id) + assert trial.state == optuna.trial.TrialState.COMPLETE + assert trial.values == [0, 1, 2] + + def test_tell_trial_fail(self) -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + trial_id = study.ask()._trial_id + + app = create_app(storage) + status, _, _ = send_request( + app, + f"/api/trials/{trial_id}/tell", + "POST", + body=json.dumps( + { + "state": "Fail", + } + ), + content_type="application/json", + ) + self.assertEqual(status, 204) + trial = storage.get_trial(trial_id) + assert trial.state == optuna.trial.TrialState.FAIL + + def test_tell_trial_with_no_state(self) -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + trial_id = study.ask()._trial_id + + app = create_app(storage) + status, _, _ = send_request( + app, + f"/api/trials/{trial_id}/tell", + "POST", + body=json.dumps({}), + content_type="application/json", + ) + self.assertEqual(status, 400) + + def test_tell_trial_with_invalid_state(self) -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + for state in ["Pruned", "Running", "Waiting", "Invalid"]: + trial_id = study.ask()._trial_id + app = create_app(storage) + with self.subTest(state=state): + status, _, _ = send_request( + app, + f"/api/trials/{trial_id}/tell", + "POST", + body=json.dumps( + { + "state": state, + } + ), + content_type="application/json", + ) + self.assertEqual(status, 400) + + def test_tell_trial_with_no_values(self) -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + trial_id = study.ask()._trial_id + + app = create_app(storage) + status, _, _ = send_request( + app, + f"/api/trials/{trial_id}/tell", + "POST", + body=json.dumps( + { + "state": "Complete", + } + ), + content_type="application/json", + ) + self.assertEqual(status, 400) + + def test_tell_trial_with_invalid_values(self) -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + for values in [1.0, ["foo"]]: + trial_id = study.ask()._trial_id + app = create_app(storage) + with self.subTest(values=values): + status, _, _ = send_request( + app, + f"/api/trials/{trial_id}/tell", + "POST", + body=json.dumps( + { + "state": "Complete", + "values": values, + } + ), + content_type="application/json", + ) + self.assertEqual(status, 400) + class BottleRequestHookTestCase(TestCase): def test_ignore_trailing_slashes(self) -> None: diff --git a/python_tests/test_cached_extra_study_property.py b/python_tests/test_cached_extra_study_property.py index c02006a7..dcd5bc5e 100644 --- a/python_tests/test_cached_extra_study_property.py +++ b/python_tests/test_cached_extra_study_property.py @@ -4,6 +4,7 @@ from typing import Any from unittest import TestCase import warnings +import numpy as np import optuna from optuna import create_trial from optuna.distributions import BaseDistribution @@ -254,11 +255,29 @@ class _CachedExtraStudyPropertyUserAttrs(TestCase): def test_infer_sortable(self) -> None: user_attrs_list: list[dict[str, Any]] = [ - {"a": 1, "b": 1, "c": 1, "d": "a", "e": 1, "f": True}, + { + "a": 1, + "b": 1, + "c": 1, + "d": "a", + "e": 1, + "f": True, + "g": np.float128(1.1), + "h": np.int64(2), + }, {"a": 2, "b": "a", "c": "a", "d": "a"}, {"a": 3, "b": None, "c": 3, "d": "a", "e": 3}, ] - expected = {"a": True, "b": False, "c": False, "d": False, "e": True, "f": False} + expected = { + "a": True, + "b": False, + "c": False, + "d": False, + "e": True, + "f": False, + "g": True, + "h": True, + } trials = [] for user_attrs in user_attrs_list: diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py new file mode 100644 index 00000000..019f18ba --- /dev/null +++ b/python_tests/test_csv_download.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from typing import Any + +import optuna +from optuna.trial import TrialState +from optuna_dashboard._app import create_app +import pytest + +from .wsgi_client import send_request + + +def _validate_output( + storage: optuna.storages.BaseStorage, + correct_status: int, + study_id: int, + expect_no_result: bool = False, + extra_col_names: list[str] | None = None, +) -> None: + app = create_app(storage) + status, _, body = send_request( + app, + f"/csv/{study_id}", + "GET", + content_type="application/json", + ) + assert status == correct_status + decoded_csv = str(body.decode("utf-8")) + if expect_no_result: + assert "is not found" in decoded_csv + else: + col_names = ["Number", "State"] + ([] if extra_col_names is None else extra_col_names) + assert all(col_name in decoded_csv for col_name in col_names) + + +def test_download_csv_no_trial() -> None: + def objective(trial: optuna.Trial) -> float: + x = trial.suggest_float("x", -100, 100) + y = trial.suggest_categorical("y", [-1, 0, 1]) + return x**2 + y + + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + study.optimize(objective, n_trials=0) + _validate_output(storage, 200, 0) + + +def test_download_csv_all_waiting() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + study.add_trial(optuna.trial.create_trial(state=TrialState.WAITING)) + _validate_output(storage, 200, 0) + + +def test_download_csv_all_running() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + study.add_trial(optuna.trial.create_trial(state=TrialState.RUNNING)) + _validate_output(storage, 200, 0) + + +@pytest.mark.parametrize("study_id", [0, 1]) +def test_download_csv_fail(study_id: int) -> None: + def objective(trial: optuna.Trial) -> float: + x = trial.suggest_float("x", -100, 100) + y = trial.suggest_categorical("y", [-1, 0, 1]) + return x**2 + y + + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + optuna.logging.set_verbosity(optuna.logging.ERROR) + study.optimize(objective, n_trials=10) + expect_no_result = study_id != 0 + cols = ["Param x", "Param y", "Value"] + _validate_output(storage, 404 if expect_no_result else 200, study_id, expect_no_result, cols) + + +@pytest.mark.parametrize("is_multi_obj", [True, False]) +def test_download_csv_multi_obj(is_multi_obj: bool) -> None: + def objective(trial: optuna.Trial) -> Any: + x = trial.suggest_float("x", -100, 100) + y = trial.suggest_categorical("y", [-1, 0, 1]) + if is_multi_obj: + return x**2, y + return x**2 + y + + storage = optuna.storages.InMemoryStorage() + directions = ["minimize", "minimize"] if is_multi_obj else ["minimize"] + study = optuna.create_study(storage=storage, directions=directions) + optuna.logging.set_verbosity(optuna.logging.ERROR) + study.optimize(objective, n_trials=10) + cols = ["Param x", "Param y"] + cols += ["Objective 0", "Objective 1"] if is_multi_obj else ["Value"] + _validate_output(storage, 200, 0, extra_col_names=cols) + + +def test_download_csv_user_attr() -> None: + def objective(trial: optuna.Trial) -> float: + x = trial.suggest_float("x", -100, 100) + y = trial.suggest_categorical("y", [-1, 0, 1]) + trial.set_user_attr("abs_y", abs(y)) + return x**2 + y + + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + optuna.logging.set_verbosity(optuna.logging.ERROR) + study.optimize(objective, n_trials=10) + cols = ["Param x", "Param y", "Value", "UserAttribute abs_y"] + _validate_output(storage, 200, 0, extra_col_names=cols) diff --git a/python_tests/test_note.py b/python_tests/test_note.py index 7cc02cd4..592cf3e7 100644 --- a/python_tests/test_note.py +++ b/python_tests/test_note.py @@ -53,3 +53,25 @@ class NoteTestCase(TestCase): note_dict = note.get_note_from_system_attrs(system_attrs, trial._trial_id) self.assertEqual(note_dict["body"], body) self.assertEqual(note_dict["version"], expected_ver) + + def test_copy_notes(self) -> None: + old_study = optuna.create_study() + old_trials = [ + old_study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) for _ in range(2) + ] + storage = old_study._storage + + notes = ["trial 0", "trial 1"] + for trial, body in zip(old_trials, notes): + save_note(trial, body) + save_note(old_study, "Study") + + new_study = optuna.create_study(storage=storage, directions=old_study.directions) + new_study.add_trials(old_study.get_trials(deepcopy=False)) + + note.copy_notes(storage, old_study, new_study) + system_attrs = new_study._storage.get_study_system_attrs(new_study._study_id) + for new_trial, body in zip(new_study.get_trials(), notes): + actual = note.get_note_from_system_attrs(system_attrs, new_trial._trial_id) + self.assertEqual(actual["body"], body) + self.assertEqual(get_note(new_study), "Study") diff --git a/python_tests/test_preferential_history.py b/python_tests/test_preferential_history.py index 3d1a7d70..dcae4797 100644 --- a/python_tests/test_preferential_history.py +++ b/python_tests/test_preferential_history.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import sys from typing import Callable from typing import TYPE_CHECKING @@ -13,6 +14,7 @@ from optuna_dashboard._preferential_history import restore_history from optuna_dashboard._serializer import serialize_preference_history from optuna_dashboard.preferential import create_study from optuna_dashboard.preferential._system_attrs import _SYSTEM_ATTR_PREFIX_PREFERENCE +import pytest from .storage_supplier import parametrize_storages from .storage_supplier import StorageSupplier @@ -22,6 +24,10 @@ if TYPE_CHECKING: from optuna_dashboard._preferential_history import History +if sys.version_info < (3, 8): + pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True) + + @parametrize_storages def test_report_and_get_choices(storage_supplier: Callable[[], StorageSupplier]) -> None: with storage_supplier() as storage: diff --git a/python_tests/test_serializers.py b/python_tests/test_serializers.py index ea1c3517..d1bdf59b 100644 --- a/python_tests/test_serializers.py +++ b/python_tests/test_serializers.py @@ -1,11 +1,15 @@ from __future__ import annotations +import sys + +import numpy as np import optuna from optuna_dashboard._serializer import serialize_attrs from optuna_dashboard._serializer import serialize_study_detail from optuna_dashboard._serializer import serialize_study_summary from optuna_dashboard._storage import get_study_summaries from optuna_dashboard.preferential import create_study +import pytest def test_serialize_bytes() -> None: @@ -22,6 +26,33 @@ def test_serialize_dict() -> None: assert len(serialized) <= 1 +def test_serialize_numpy_integer() -> None: + serialized = serialize_attrs( + { + "int8": np.int8(1), + "int16": np.int16(1), + "int32": np.int32(1), + "int64": np.int64(1), + } + ) + assert len(serialized) == 4 + assert all([v["value"] == "1" for v in serialized]) + + +def test_serialize_numpy_floating() -> None: + serialized = serialize_attrs( + { + "float16": np.float16(1.0), + "float32": np.float32(1.0), + "float64": np.float64(1.0), + "float128": np.float128(1.0), + } + ) + assert len(serialized) == 4 + assert all([v["value"] == "1.0" for v in serialized]) + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_get_study_detail_is_preferential() -> None: storage = optuna.storages.InMemoryStorage() study = create_study(n_generate=4, storage=storage) @@ -48,6 +79,7 @@ def test_get_study_detail_is_not_preferential() -> None: assert not study_detail["is_preferential"] +@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_get_study_summary_is_preferential() -> None: storage = optuna.storages.InMemoryStorage() create_study(n_generate=4, storage=storage) diff --git a/rustlib/src/lib.rs b/rustlib/src/lib.rs index ff09ef81..c326e503 100644 --- a/rustlib/src/lib.rs +++ b/rustlib/src/lib.rs @@ -1,16 +1,20 @@ +use fanova::{FanovaOptions, RandomForestOptions}; use js_sys::Array; use serde_wasm_bindgen::from_value; -use fanova::{FanovaOptions, RandomForestOptions}; use wasm_bindgen::prelude::*; #[wasm_bindgen] -pub fn wasm_fanova_calculate(features: Array, targets: Array) -> Vec { - // TODO(c-bata): Fix error handling +pub fn wasm_fanova_calculate(features: Array, targets: Array) -> Result, JsError> { let features_vec: Vec> = features .iter() - .map(|x| from_value::>(x).unwrap()) - .collect(); - let targets_vec: Vec = targets.iter().map(|x| x.as_f64().unwrap()).collect(); + .map(|x| from_value::>(x)) + .collect::>() + .map_err(|_| JsError::new("features must be of type number[][]"))?; + let targets_vec: Vec = targets + .iter() + .map(|x| x.as_f64()) + .collect::>() + .ok_or(JsError::new("targets must be of type number[]"))?; let mut fanova = FanovaOptions::new() .random_forest(RandomForestOptions::new().seed(0)) @@ -18,9 +22,10 @@ pub fn wasm_fanova_calculate(features: Array, targets: Array) -> Vec { features_vec.iter().map(|x| x.as_slice()).collect(), &targets_vec, ) - .unwrap(); + .map_err(|e| JsError::new(&format!("failed to build fANOVA model: {}", e)))?; let importances = (0..features_vec.len()) .map(|i| fanova.quantify_importance(&[i]).mean) .collect::>(); - return importances; + + Ok(importances) } diff --git a/standalone_app/src/components/DataGrid.tsx b/standalone_app/src/components/DataGrid.tsx index 2d6a5670..2855f1f7 100644 --- a/standalone_app/src/components/DataGrid.tsx +++ b/standalone_app/src/components/DataGrid.tsx @@ -28,7 +28,7 @@ interface DataGridColumn { field: keyof T label: string sortable?: boolean - less?: (a: T, b: T) => number + less?: (a: T, b: T, ascending: boolean) => number filterable?: boolean toCellValue?: (rowIndex: number) => string | React.ReactNode padding?: "normal" | "checkbox" | "none" @@ -358,7 +358,10 @@ function stableSort( const stabilizedThis = array.map((el, index) => [el, index] as [T, number]) stabilizedThis.sort((a, b) => { if (less) { - const result = order == "asc" ? -less(a[0], b[0]) : less(a[0], b[0]) + const ascending = order === "asc" + const result = ascending + ? -less(a[0], b[0], ascending) + : less(a[0], b[0], ascending) if (result !== 0) return result } else { const result = comparator(a[0], b[0]) diff --git a/standalone_app/src/components/PlotImportance.tsx b/standalone_app/src/components/PlotImportance.tsx index 24e9cd8f..7303b2db 100644 --- a/standalone_app/src/components/PlotImportance.tsx +++ b/standalone_app/src/components/PlotImportance.tsx @@ -39,6 +39,7 @@ export const PlotImportance: FC<{ study: Study }> = ({ study }) => { const values = filteredTrials.map( (t) => t.values?.[objectiveId] as number ) + // TODO: handle errors thrown by wasm_fanova_calculate const importance = wasm_fanova_calculate(features, values) return study.intersection_search_space.map((s, i) => ({ name: s.name, diff --git a/standalone_app/src/components/PlotIntermediateValues.tsx b/standalone_app/src/components/PlotIntermediateValues.tsx index 4feb0b33..8418cba6 100644 --- a/standalone_app/src/components/PlotIntermediateValues.tsx +++ b/standalone_app/src/components/PlotIntermediateValues.tsx @@ -78,7 +78,7 @@ const plotIntermediateValue = ( t.state === "Pruned" && t.values && t.values.length > 0) || - t.state == "Running" + t.state === "Running" ) const plotData: Partial[] = filteredTrials.map((trial) => { const values = trial.intermediate_values.filter( diff --git a/standalone_app/src/components/TrialTable.tsx b/standalone_app/src/components/TrialTable.tsx index 58d2d101..d8b4f163 100644 --- a/standalone_app/src/components/TrialTable.tsx +++ b/standalone_app/src/components/TrialTable.tsx @@ -20,12 +20,12 @@ export const TrialTable: FC<{ }, ] - if (study === null || study.directions.length == 1) { + if (study === null || study.directions.length === 1) { columns.push({ field: "values", label: "Value", sortable: true, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, ascending): number => { const firstVal = firstEl.values?.[0] const secondVal = secondEl.values?.[0] @@ -33,9 +33,9 @@ export const TrialTable: FC<{ return 0 } if (firstVal === undefined) { - return -1 + return ascending ? -1 : 1 } else if (secondVal === undefined) { - return 1 + return ascending ? 1 : -1 } if (firstVal === "-inf" || secondVal === "inf") { return 1 @@ -57,7 +57,7 @@ export const TrialTable: FC<{ field: "values", label: `Objective ${objectiveId}`, sortable: true, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, ascending): number => { const firstVal = firstEl.values?.[objectiveId] const secondVal = secondEl.values?.[objectiveId] @@ -65,9 +65,9 @@ export const TrialTable: FC<{ return 0 } if (firstVal === undefined) { - return -1 + return ascending ? -1 : 1 } else if (secondVal === undefined) { - return 1 + return ascending ? 1 : -1 } if (firstVal === "-inf" || secondVal === "inf") { return 1 @@ -96,7 +96,8 @@ export const TrialTable: FC<{ null, sortable: true, filterable: false, - less: (firstEl, secondEl): number => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + less: (firstEl, secondEl, _): number => { const firstVal = firstEl.params.find( (p) => p.name === s.name )?.param_internal_value @@ -126,7 +127,8 @@ export const TrialTable: FC<{ ?.value || null, sortable: attr_spec.sortable, filterable: false, - less: (firstEl, secondEl): number => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + less: (firstEl, secondEl, _): number => { const firstVal = firstEl.user_attrs.find( (attr) => attr.key === attr_spec.key )?.value diff --git a/standalone_app/src/sqlite3.ts b/standalone_app/src/sqlite3.ts index 3a04c163..1fbcff87 100644 --- a/standalone_app/src/sqlite3.ts +++ b/standalone_app/src/sqlite3.ts @@ -64,7 +64,7 @@ const getSchemaVersion = (db: SQLite3DB): string => { const isSupportedSchema = (schemaVersion: string): boolean => { const lowestVersion = "v2.6.0.a" // supported: "v3.2.0.a", "v3.0.0.{a,b,c,d}", "v2.6.0.a" - if (schemaVersion == lowestVersion) return true + if (schemaVersion === lowestVersion) return true return isGreaterSchemaVersion(schemaVersion, lowestVersion) } @@ -80,7 +80,7 @@ const isGreaterSchemaVersion = ( const left = Number(leftVersion) const right = Number(rightVersion) - if (left == right) return leftSuffix > rightSuffix + if (left === right) return leftSuffix > rightSuffix return left > right } @@ -106,7 +106,7 @@ const getStudies = (db: SQLite3DB, schemaVersion: string): Study[] => { trials.forEach((trial) => { const userAttrs = getTrialUserAttributes(db, trial.trial_id) userAttrs.forEach((attr) => { - if (union_user_attrs.findIndex((s) => s.key === attr.key) == -1) { + if (union_user_attrs.findIndex((s) => s.key === attr.key) === -1) { union_user_attrs.push({ key: attr.key, sortable: false }) } }) @@ -116,7 +116,7 @@ const getStudies = (db: SQLite3DB, schemaVersion: string): Study[] => { params.forEach((param) => { param_names.add(param.name) if ( - union_search_space.findIndex((s) => s.name === param.name) == -1 + union_search_space.findIndex((s) => s.name === param.name) === -1 ) { union_search_space.push({ name: param.name }) } diff --git a/typescript_tests/DataGrid.test.tsx b/typescript_tests/DataGrid.test.tsx index 475e360a..6d13deb7 100644 --- a/typescript_tests/DataGrid.test.tsx +++ b/typescript_tests/DataGrid.test.tsx @@ -1,7 +1,7 @@ import React from "react" global.URL.createObjectURL = jest.fn() -import { cleanup, render, fireEvent } from "@testing-library/react" +import { cleanup, render } from "@testing-library/react" import { DataGrid, DataGridColumn, @@ -9,6 +9,7 @@ import { afterEach(cleanup) +// TODO(c-bata): Add tests to check filterChoices option it("Filter rows of DataGrid", () => { interface DummyAttribute { id: number @@ -23,7 +24,7 @@ it("Filter rows of DataGrid", () => { { id: 5, key: "foo", value: 3 }, ] const columns: DataGridColumn[] = [ - { field: "key", label: "Key", filterable: true }, + { field: "key", label: "Key" }, { field: "value", label: "Value", @@ -39,46 +40,4 @@ it("Filter rows of DataGrid", () => { /> ) expect(queryAllByText("bar").length).toBe(2) - - // Filter rows by "foo" - fireEvent.click(queryAllByText("foo")[0]) - expect(queryAllByText("foo").length).toBe(3) - expect(queryAllByText("bar").length).toBe(0) -}) - -it("Filter rows after sorted", () => { - interface DummyAttribute { - id: number - key: string - value: number - } - const dummyAttributes = [ - { id: 1, key: "foo", value: 4000 }, - { id: 2, key: "bar", value: 1000 }, - { id: 3, key: "bar", value: 2000 }, - { id: 4, key: "foo", value: 3000 }, - { id: 5, key: "foo", value: 5000 }, - ] - const columns: DataGridColumn[] = [ - { field: "key", label: "Key", filterable: true }, - { - field: "value", - label: "Value", - sortable: true, - }, - ] - - const { getByText, queryAllByText } = render( - - columns={columns} - rows={dummyAttributes} - keyField={"id"} - /> - ) - // Sort and filter rows - fireEvent.click(getByText("Value")) - fireEvent.click(queryAllByText("bar")[0]) - - expect(queryAllByText("1000").length).toBe(1) - expect(queryAllByText("2000").length).toBe(1) }) diff --git a/vscode/README.md b/vscode/README.md index 78587c37..960669a3 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -14,6 +14,10 @@ Nothing to configure. ## Release Notes +### 0.1.0 + +Added older database schemas support (Optuna 2.6.0 or later) + ### 0.0.1 Initial Release diff --git a/vscode/package.json b/vscode/package.json index a5c1d818..6a99a905 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -3,7 +3,7 @@ "displayName": "Optuna Dashboard", "description": "Web Dashboard for Optuna", "publisher": "Optuna", - "version": "0.0.1", + "version": "0.1.0", "license": "MIT", "icon": "images/optuna-logo.png", "engines": {