summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-server
diff options
context:
space:
mode:
authorvincent van beek <vincent@vlogic.nl>2026-03-26 14:02:54 +0100
committerGitHub <noreply@github.com>2026-03-26 13:02:54 +0000
commit0ffde21b0337c606e2d0ece5bd5434a930a87dcd (patch)
tree4fd071728a8da6dcf3e6fc9fe9dca5a492100d34 /opendc-web/opendc-web-server
parent8bb98c773bc982a0dab9cf9fb20d62f60a36a5d7 (diff)
Use Quarkus Quinoa for serving web UI (#391)
* refactor(web): Migrate to Quarkus 3 This commit updates the OpenDC web server to use Quarkus 3, which changes annotations to use the Jakarta namespace for annotations. * refactor(web): Configure runtime variables for web UI This commit updates the web UI to propagate runtime variables via the next-runtime-env package. Before, we would need to replace the variables in the generated sources by Next.js, next-runtime-env works by loading a JavaScript file when opening the OpenDC web UI which contains the configuration of the web app. * refactor(web): Migrate to Quarkus Quinoa This commit updates the OpenDC web server to make use of Quarkus Quinoa for serving the web UI. This allows us to deprecate the complex Quarkus extension for serving the web UI. * refactor(web): Move web UI into Quarkus web app This commit moves the web UI into the Quarkus web server module to ensure we follow Quarkus Quinoa's conventions. * refactor(web): Merge Quarkus extension into single module This commit merges the existing Quarkus extensions into a single module to prevent build complexity. * refactor(web): Migrate web proto to Java This commit migrates the web protocol to Java and removes the dependency on Jandex Gradle. * refactor(web): Migrate to Quarkus 3 This commit updates the OpenDC web server to use Quarkus 3, which changes annotations to use the Jakarta namespace for annotations. * enable DB schema migration on DEV server * webui is not needed anymore * remove MAINTAINERS is depricated * fix quarkus.quinoa properties * revert properties change, install npm in docker image to allow building the frontend * pin postgres version, this is a best practice. Fix some properties the old ones are depricated. Added properties for local testing * fix build error * :opendc-web:opendc-web-proto:spotlessApply * fix database schema --------- Co-authored-by: Fabian Mastenbroek <mail.fabianm@gmail.com>
Diffstat (limited to 'opendc-web/opendc-web-server')
-rw-r--r--opendc-web/opendc-web-server/Dockerfile7
-rw-r--r--opendc-web/opendc-web-server/build.gradle.kts11
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java11
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java1
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java4
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java38
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java18
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java5
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java2
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java2
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java14
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java2
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java4
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application-dev.properties10
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application-docker.properties11
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application.properties3
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/load_data.sql58
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/.dockerignore9
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/.eslintrc16
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/.gitignore28
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/.prettierrc.yaml5
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/Dockerfile27
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/README.md106
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/api/index.js (renamed from opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java)50
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/api/portfolios.js39
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/api/projects.js39
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/api/scenarios.js39
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/api/schedulers.js27
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/api/topologies.js44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/api/traces.js27
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/api/users.js32
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/auth.js97
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/AppHeader.js69
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/AppHeader.module.css42
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/AppHeaderTools.js93
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/AppHeaderUser.js99
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/AppPage.js44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelectionSection.js34
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelectionSection.module.css28
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelector.js79
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelector.module.css44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/context/PortfolioSelector.js52
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/context/ProjectSelector.js55
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/context/TopologySelector.js52
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenario.js60
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenarioModal.js157
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioOverview.js120
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResultInfo.js40
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResults.js180
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioState.js62
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js103
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/FilterPanel.js26
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/FilterPanel.module.css7
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/NewPortfolio.js53
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/NewPortfolioModal.js161
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopology.js57
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopologyModal.js115
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/PortfolioTable.js99
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectCollection.js137
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectOverview.js98
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/projects/TopologyTable.js115
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/RoomTable.js74
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/TopologyMap.js69
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/TopologyOverview.js92
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/GrayContainer.js34
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapConstants.js25
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapStage.js83
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapStage.module.css29
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackContainer.js37
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackEnergyFillContainer.js36
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackSpaceFillContainer.js42
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RoomContainer.js54
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/TileContainer.js50
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/TopologyContainer.js34
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/WallContainer.js39
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Collapse.js42
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Collapse.module.css55
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/ScaleIndicator.js18
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/ScaleIndicator.module.css10
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Toolbar.js33
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Toolbar.module.css27
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/Backdrop.js10
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/GrayLayer.js24
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/HoverTile.js30
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/ImageComponent.js37
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/RackFillBar.js68
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/RoomTile.js24
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/TileObject.js27
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/TilePlusIcon.js44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/WallSegment.js32
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/GridGroup.js36
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/RackGroup.js25
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/RoomGroup.js52
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/TileGroup.js36
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/TopologyGroup.js44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/WallGroup.js22
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/HoverLayerComponent.js55
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/MapLayer.js41
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/ObjectHoverLayer.js51
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/RoomHoverLayer.js59
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/NameComponent.js69
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.js83
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.module.css35
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/BuildingSidebar.js8
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionComponent.js46
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionContainer.js46
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/DeleteMachine.js59
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/MachineSidebar.js55
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddComponent.js42
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddContainer.js44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListComponent.js113
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListContainer.js47
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitTabsComponent.js36
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitType.js25
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js41
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/DeleteRackContainer.js60
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineComponent.js40
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListComponent.js80
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListContainer.js56
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackNameContainer.js22
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.js58
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.module.css14
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/DeleteRoomContainer.js59
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/EditRoomContainer.js61
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js35
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js46
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomName.js44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomSidebar.js43
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/util/TableEmptyState.js103
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/util/modals/ConfirmationModal.js27
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/util/modals/Modal.js38
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/util/modals/TextInputModal.js70
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/config.js43
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/data/experiments.js47
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/data/project.js166
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/data/query.js59
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/data/topology.js88
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/data/user.js40
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/next.config.js46
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/package-lock.json8146
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/package.json77
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/pages/404.js38
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/pages/_app.js108
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/pages/_document.js78
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/pages/logout.js39
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/index.js75
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/portfolios/[portfolio].js121
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/topologies/[topology].js142
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/pages/projects/index.js116
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/favicon.icobin0 -> 99678 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/humans.txt35
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/avatar.svg18
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/datacenter-drawing.pngbin0 -> 207909 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/logo.pngbin0 -> 2825 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/logo.svg191
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/opendc-architecture.pngbin0 -> 45056 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/opendc-timeline-v2.pngbin0 -> 33460 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/aiosup.pngbin0 -> 71879 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/evaneyk.pngbin0 -> 89028 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/fmastenbroek.pngbin0 -> 123006 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/gandreadis.pngbin0 -> 76426 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/hhe.pngbin0 -> 102718 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jbosch.pngbin0 -> 101618 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jburley.pngbin0 -> 328112 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/lfdversluis.pngbin0 -> 67796 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/loverweel.pngbin0 -> 65866 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/sjounaid.pngbin0 -> 94523 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/vvanbeek.pngbin0 -> 85159 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/portraits/wlai.pngbin0 -> 72873 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-construction.pngbin0 -> 275103 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-simulation.pngbin0 -> 291836 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Developer.pngbin0 -> 11411 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Manager.pngbin0 -> 9946 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Researcher.pngbin0 -> 10984 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Sales.pngbin0 -> 10074 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Student.pngbin0 -> 12960 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/topology/cpu-icon.pngbin0 -> 4062 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/topology/gpu-icon.pngbin0 -> 2227 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/topology/memory-icon.pngbin0 -> 1980 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-energy-icon.pngbin0 -> 893 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-space-icon.pngbin0 -> 957 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/topology/storage-icon.pngbin0 -> 4038 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/img/tudelft-icon.pngbin0 -> 4387 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/manifest.json15
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/public/robots.txt3
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/actions/interaction-level.js57
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/building.js113
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/index.js40
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/machine.js28
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/rack.js36
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js74
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/index.js59
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js43
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/reducers/index.js12
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/reducers/interaction-level.js68
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/index.js44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js47
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/rack.js66
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/room.js65
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/tile.js58
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/topology.js47
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/sagas/index.js7
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/redux/sagas/topology.js76
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/shapes.js187
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/style/index.css28
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/authorizations.js21
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/available-metrics.js101
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/colors.js29
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/date-time.js81
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/date-time.test.js21
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/effect-ref.js41
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/tile-calculations.js255
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/topology-schema.js47
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/unit-specifications.js102
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java10
215 files changed, 17719 insertions, 107 deletions
diff --git a/opendc-web/opendc-web-server/Dockerfile b/opendc-web/opendc-web-server/Dockerfile
index 2aee7ddf..7e8d34c3 100644
--- a/opendc-web/opendc-web-server/Dockerfile
+++ b/opendc-web/opendc-web-server/Dockerfile
@@ -1,6 +1,4 @@
FROM eclipse-temurin:21-jdk-jammy
-MAINTAINER OpenDC Maintainers <opendc@atlarge-research.com>
-
# Obtain (cache) Gradle wrapper
COPY gradlew /app/
COPY gradle /app/gradle
@@ -17,7 +15,10 @@ ENV OPENDC_AUTH0_AUDIENCE=$OPENDC_AUTH0_AUDIENCE
ENV OPENDC_AUTH0_DOCS_CLIENT_ID=$OPENDC_AUTH0_DOCS_CLIENT_ID
COPY ./ /app/
-RUN ./gradlew --no-daemon :opendc-web:opendc-web-server:quarkusBuild -Dquarkus.profile=docker
+RUN curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash && \
+ . ~/.nvm/nvm.sh && \
+ nvm install --lts && \
+ ./gradlew --no-daemon :opendc-web:opendc-web-server:quarkusBuild -Dquarkus.profile=docker
FROM eclipse-temurin:21-jdk-jammy
COPY --from=0 /app/opendc-web/opendc-web-server/build/quarkus-app /opt/opendc
diff --git a/opendc-web/opendc-web-server/build.gradle.kts b/opendc-web/opendc-web-server/build.gradle.kts
index 484e98c0..3a99f3ce 100644
--- a/opendc-web/opendc-web-server/build.gradle.kts
+++ b/opendc-web/opendc-web-server/build.gradle.kts
@@ -32,12 +32,8 @@ dependencies {
implementation(enforcedPlatform(libs.quarkus.bom))
implementation(projects.opendcWeb.opendcWebProto)
- testImplementation("junit:junit:4.13.1")
- testImplementation("junit:junit:4.13.1")
- compileOnly(projects.opendcWeb.opendcWebUiQuarkusDeployment) // Temporary fix for Quarkus/Gradle issues
- compileOnly(projects.opendcWeb.opendcWebRunnerQuarkusDeployment)
- implementation(projects.opendcWeb.opendcWebUiQuarkus)
- implementation(projects.opendcWeb.opendcWebRunnerQuarkus)
+ compileOnly(projects.opendcWeb.opendcWebQuarkusDeployment)
+ implementation(projects.opendcWeb.opendcWebQuarkus)
implementation(libs.quarkus.kotlin)
implementation(libs.quarkus.resteasy.core)
@@ -47,6 +43,7 @@ dependencies {
implementation(libs.quarkus.security)
implementation(libs.quarkus.oidc)
+ implementation(libs.quarkus.quinoa.runtime)
implementation(libs.quarkus.hibernate.orm.core)
implementation(libs.quarkus.hibernate.orm.panache)
@@ -68,7 +65,7 @@ val createStartScripts by tasks.creating(CreateStartScripts::class) {
applicationName = "opendc-server"
mainClass.set("io.quarkus.bootstrap.runner.QuarkusEntryPoint")
classpath = files("lib/quarkus-run.jar")
- outputDir = project.layout.buildDirectory.get().asFile.resolve("scripts")
+ outputDir = project.layout.buildDirectory.dir("scripts").get().asFile
}
distributions {
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java
index a0ac390f..ef342e5f 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java
@@ -28,6 +28,17 @@ import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.panache.common.Parameters;
import jakarta.persistence.*;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.ForeignKey;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.NamedQueries;
+import jakarta.persistence.NamedQuery;
+import jakarta.persistence.Table;
import java.time.Instant;
import java.util.Map;
import org.hibernate.annotations.Type;
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java
index c2695192..80031c0a 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java
@@ -51,6 +51,7 @@ import org.opendc.web.proto.Targets;
*/
@Entity
@Table(
+ name = "portfolios",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_portfolios_number",
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java
index f4e5305d..ca032e21 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java
@@ -44,7 +44,7 @@ import java.util.Set;
* A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users.
*/
@Entity
-@Table
+@Table(name = "projects")
@NamedQueries({
@NamedQuery(
name = "Project.findByUser",
@@ -52,7 +52,7 @@ import java.util.Set;
"""
SELECT a
FROM ProjectAuthorization a
- WHERE a.key.userName = :userName
+ WHERE a.key.userId = :userId
"""),
@NamedQuery(
name = "Project.allocatePortfolio",
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java
index 3776ae12..ad94ad29 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java
@@ -47,7 +47,7 @@ import org.opendc.web.proto.user.ProjectRole;
* An authorization for some user to participate in a project.
*/
@Entity
-@Table
+@Table(name = "project_authorizations")
@NamedQueries({
@NamedQuery(
name = "ProjectAuthorization.findByUser",
@@ -55,7 +55,7 @@ import org.opendc.web.proto.user.ProjectRole;
"""
SELECT a
FROM ProjectAuthorization a
- WHERE a.key.userName = :userName
+ WHERE a.key.userId = :userId
"""),
})
public class ProjectAuthorization extends PanacheEntityBase {
@@ -88,8 +88,8 @@ public class ProjectAuthorization extends PanacheEntityBase {
/**
* Construct a {@link ProjectAuthorization} object.
*/
- public ProjectAuthorization(Project project, String userName, ProjectRole role) {
- this.key = new ProjectAuthorization.Key(project.id, userName);
+ public ProjectAuthorization(Project project, String userId, ProjectRole role) {
+ this.key = new ProjectAuthorization.Key(project.id, userId);
this.project = project;
this.role = role;
}
@@ -100,25 +100,25 @@ public class ProjectAuthorization extends PanacheEntityBase {
protected ProjectAuthorization() {}
/**
- * List all projects for the user with the specified <code>userName</code>.
+ * List all projects for the user with the specified <code>userId</code>.
*
- * @param userName The identifier of the user that is requesting the list of projects.
+ * @param userId The identifier of the user that is requesting the list of projects.
* @return A query returning projects that the user has received authorization for.
*/
- public static PanacheQuery<ProjectAuthorization> findByUser(String userName) {
- return find("#ProjectAuthorization.findByUser", Parameters.with("userName", userName));
+ public static PanacheQuery<ProjectAuthorization> findByUser(String userId) {
+ return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId));
}
/**
- * Find the project with <code>id</code> for the user with the specified <code>userName</code>.
+ * Find the project with <code>id</code> for the user with the specified <code>userId</code>.
*
- * @param userName The identifier of the user that is requesting the list of projects.
- * @param project_id The unique identifier of the project.
+ * @param userId The identifier of the user that is requesting the list of projects.
+ * @param id The unique identifier of the project.
* @return The project with the specified identifier or <code>null</code> if it does not exist or is not accessible
* to the user with the specified identifier.
*/
- public static ProjectAuthorization findByUser(String userName, long project_id) {
- return findById(new ProjectAuthorization.Key(project_id, userName));
+ public static ProjectAuthorization findByUser(String userId, long id) {
+ return findById(new ProjectAuthorization.Key(id, userId));
}
/**
@@ -146,12 +146,12 @@ public class ProjectAuthorization extends PanacheEntityBase {
@Column(name = "project_id", nullable = false)
public long projectId;
- @Column(name = "user_name", nullable = false)
- public String userName;
+ @Column(name = "user_id", nullable = false)
+ public String userId;
- public Key(long projectId, String userName) {
+ public Key(long projectId, String userId) {
this.projectId = projectId;
- this.userName = userName;
+ this.userId = userId;
}
protected Key() {}
@@ -161,12 +161,12 @@ public class ProjectAuthorization extends PanacheEntityBase {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
- return projectId == key.projectId && userName.equals(key.userName);
+ return projectId == key.projectId && userId.equals(key.userId);
}
@Override
public int hashCode() {
- return Objects.hash(projectId, userName);
+ return Objects.hash(projectId, userId);
}
}
}
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java
index c79ef5bb..0224ae43 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java
@@ -27,6 +27,20 @@ import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.panache.common.Parameters;
import jakarta.persistence.*;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Embedded;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.ForeignKey;
+import jakarta.persistence.Index;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.NamedQueries;
+import jakarta.persistence.NamedQuery;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.annotations.Type;
@@ -37,6 +51,7 @@ import org.opendc.web.proto.OperationalPhenomena;
*/
@Entity
@Table(
+ name = "scenarios",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_scenarios_number",
@@ -109,13 +124,10 @@ public class Scenario extends PanacheEntityBase {
/**
* Operational phenomena activated in the scenario.
- * @Column(columnDefinition = "jsonb", nullable = false, updatable = false)
- * @Type(JsonType.class)
*/
@Column(columnDefinition = "jsonb", nullable = false, updatable = false)
@Type(JsonType.class)
public OperationalPhenomena phenomena;
-
/**
* The name of the VM scheduler used in the scenario.
*/
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java
index 8a4e2ae2..ff8b4416 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java
@@ -41,13 +41,14 @@ import jakarta.persistence.UniqueConstraint;
import java.time.Instant;
import java.util.List;
import org.hibernate.annotations.Type;
-import org.opendc.web.proto.Room;
+import org.opendc.web.proto.topology.Room;
/**
* A datacenter design in OpenDC.
*/
@Entity
@Table(
+ name = "topologies",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_topologies_number",
@@ -103,8 +104,6 @@ public class Topology extends PanacheEntityBase {
/**
* Datacenter design in JSON
- * @Column(columnDefinition = "jsonb", nullable = false)
- * @Type(JsonType.class)
*/
@Column(columnDefinition = "jsonb", nullable = false)
@Type(JsonType.class)
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java
index 4dde8654..2b774082 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java
@@ -98,7 +98,7 @@ public final class JobResource {
}
try {
- jobService.updateJob(job, update.getState(), update.getRuntime(), update.getResults());
+ jobService.updateJob(job, update.state(), update.runtime(), update.results());
} catch (IllegalArgumentException e) {
throw new WebApplicationException(e, 400);
} catch (IllegalStateException e) {
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java
index 2a3a40f4..e4d5362c 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java
@@ -100,7 +100,7 @@ public final class PortfolioResource {
var project = auth.project;
int number = project.allocatePortfolio(now);
- Portfolio portfolio = new Portfolio(project, number, request.getName(), request.getTargets());
+ Portfolio portfolio = new Portfolio(project, number, request.name(), request.targets());
project.portfolios.add(portfolio);
portfolio.persist();
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java
index 789808c8..ea87a7ad 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java
@@ -118,12 +118,12 @@ public final class PortfolioScenarioResource {
throw new WebApplicationException("Portfolio not found", 404);
}
- Topology topology = Topology.findByProject(projectId, (int) request.getTopology());
+ Topology topology = Topology.findByProject(projectId, (int) request.topology());
if (topology == null) {
throw new WebApplicationException("Referred topology does not exist", 400);
}
- Trace trace = Trace.findById(request.getWorkload().getTrace());
+ Trace trace = Trace.findById(request.workload().trace());
if (trace == null) {
throw new WebApplicationException("Referred trace does not exist", 400);
}
@@ -136,14 +136,14 @@ public final class PortfolioScenarioResource {
project,
portfolio,
number,
- request.getName(),
- new Workload(trace, request.getWorkload().getSamplingFraction()),
+ request.name(),
+ new Workload(trace, request.workload().samplingFraction()),
topology,
- request.getPhenomena(),
- request.getSchedulerName());
+ request.phenomena(),
+ request.schedulerName());
scenario.persist();
- Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats());
+ Job job = new Job(scenario, userId, now, portfolio.targets.repeats());
job.persist();
// Fail the job if there is not enough budget for the simulation
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java
index ae1c959e..40ebc666 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java
@@ -79,7 +79,7 @@ public final class ProjectResource {
@Consumes("application/json")
public org.opendc.web.proto.user.Project create(@Valid org.opendc.web.proto.user.Project.Create request) {
Instant now = Instant.now();
- Project entity = new Project(request.getName(), now);
+ Project entity = new Project(request.name(), now);
entity.persist();
ProjectAuthorization authorization =
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java
index b8c542d3..25819e32 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java
@@ -104,7 +104,7 @@ public final class TopologyResource {
Project project = auth.project;
int number = project.allocateTopology(now);
- Topology topology = new Topology(project, number, request.getName(), now, request.getRooms());
+ Topology topology = new Topology(project, number, request.name(), now, request.rooms());
project.topologies.add(topology);
topology.persist();
@@ -164,7 +164,7 @@ public final class TopologyResource {
}
entity.updatedAt = Instant.now();
- entity.rooms = request.getRooms();
+ entity.rooms = request.rooms();
return UserProtocol.toDto(entity, auth);
}
diff --git a/opendc-web/opendc-web-server/src/main/resources/application-dev.properties b/opendc-web/opendc-web-server/src/main/resources/application-dev.properties
index 5fbc4c04..98b1e9eb 100644
--- a/opendc-web/opendc-web-server/src/main/resources/application-dev.properties
+++ b/opendc-web/opendc-web-server/src/main/resources/application-dev.properties
@@ -24,11 +24,21 @@ quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TY
# Hibernate
quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
+quarkus.hibernate-orm.log.sql=true
quarkus.flyway.clean-at-start=true
+quarkus.flyway.locations=db/migration,db/testing
# Disable authentication
+quarkus.oidc.enabled=false
opendc.security.enabled=false
+# Create new tables and fill them
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.hibernate-orm.sql-load-script=load_data.sql
+
+# Quinoa
+quarkus.quinoa.dev-server=true
+
# Mount web UI at root and API at "/api"
quarkus.resteasy.path=/api
diff --git a/opendc-web/opendc-web-server/src/main/resources/application-docker.properties b/opendc-web/opendc-web-server/src/main/resources/application-docker.properties
index eae9ee1e..f0b3e7dc 100644
--- a/opendc-web/opendc-web-server/src/main/resources/application-docker.properties
+++ b/opendc-web/opendc-web-server/src/main/resources/application-docker.properties
@@ -27,10 +27,7 @@ quarkus.datasource.password=${OPENDC_DB_PASSWORD}
quarkus.datasource.jdbc.url=${OPENDC_DB_URL}
# Hibernate
-quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQL95Dialect
-
-# Disable OpenDC web UI
-quarkus.opendc-ui.include=false
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect
# Security
opendc.security.enabled=true
@@ -47,3 +44,9 @@ quarkus.smallrye-openapi.security-scheme=oidc
quarkus.smallrye-openapi.security-scheme-name=Auth0
quarkus.smallrye-openapi.oidc-open-id-connect-url=https://${OPENDC_AUTH0_DOMAIN:opendc.eu.auth0.com}/.well-known/openid-configuration
quarkus.smallrye-openapi.servers=https://api.opendc.org
+
+# Enable the settings below if you want to test the docker-compose deployment locally
+#quarkus.hibernate-orm.database.generation=drop-and-create
+#quarkus.resteasy.path=/api
+#quarkus.oidc.enabled=false
+#opendc.security.enabled=false
diff --git a/opendc-web/opendc-web-server/src/main/resources/application.properties b/opendc-web/opendc-web-server/src/main/resources/application.properties
index 0f47db30..8daeccf3 100644
--- a/opendc-web/opendc-web-server/src/main/resources/application.properties
+++ b/opendc-web/opendc-web-server/src/main/resources/application.properties
@@ -22,6 +22,9 @@
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:3000,https://opendc.org
+# Quinoa
+quarkus.quinoa.dev-server=false
+
# Security
quarkus.oidc.enabled=${opendc.security.enabled}
diff --git a/opendc-web/opendc-web-server/src/main/resources/load_data.sql b/opendc-web/opendc-web-server/src/main/resources/load_data.sql
index 72396cef..39cb3a02 100644
--- a/opendc-web/opendc-web-server/src/main/resources/load_data.sql
+++ b/opendc-web/opendc-web-server/src/main/resources/load_data.sql
@@ -1,56 +1,56 @@
-- Insert data
-INSERT INTO PROJECT (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
+INSERT INTO projects (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Project 1', 0, 0, 0, '2024-03-01T15:31:41.579969Z', 1);
-INSERT INTO PROJECTAUTHORIZATION (role, project_id, user_name)
+INSERT INTO project_authorizations (role, project_id, user_id)
VALUES ('OWNER', 1, 'test_user_1');
-- Add test user 2 as a viewer for project 1
-INSERT INTO PROJECTAUTHORIZATION (role, project_id, user_name)
+INSERT INTO project_authorizations (role, project_id, user_id)
VALUES ('VIEWER', 1, 'test_user_2');
-- Add test user 3 as an editor for project 1
-INSERT INTO PROJECTAUTHORIZATION (role, project_id, user_name)
+INSERT INTO project_authorizations (role, project_id, user_id)
VALUES ('EDITOR', 1, 'test_user_3');
-- Create a project for test user 2
-INSERT INTO PROJECT (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
+INSERT INTO projects (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Project 2', 0, 0, 0, '2024-03-01T15:31:41.579969Z', 2);
-INSERT INTO PROJECTAUTHORIZATION (role, project_id, user_name)
+INSERT INTO project_authorizations (role, project_id, user_id)
VALUES ('OWNER', 2, 'test_user_2');
-- Create three projects for test user 3. User 3 has multiple projects to test getAll
-INSERT INTO PROJECT (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
+INSERT INTO projects (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Project 3', 0, 0, 0, '2024-03-01T15:31:41.579969Z', 3);
-INSERT INTO PROJECTAUTHORIZATION (role, project_id, user_name)
+INSERT INTO project_authorizations (role, project_id, user_id)
VALUES ('OWNER', 3, 'test_user_3');
-INSERT INTO PROJECT (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
+INSERT INTO projects (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Project 4', 0, 0, 0, '2024-03-01T15:31:41.579969Z', 4);
-INSERT INTO PROJECTAUTHORIZATION (role, project_id, user_name)
+INSERT INTO project_authorizations (role, project_id, user_id)
VALUES ('OWNER', 4, 'test_user_3');
-INSERT INTO PROJECT (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
+INSERT INTO projects (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Project 5', 0, 0, 0, '2024-03-01T15:31:41.579969Z', 5);
-INSERT INTO PROJECTAUTHORIZATION (role, project_id, user_name)
+INSERT INTO project_authorizations (role, project_id, user_id)
VALUES ('OWNER', 5, 'test_user_3');
-- Project to delete
-INSERT INTO PROJECT (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
+INSERT INTO projects (created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Project Delete', 0, 0, 0, '2024-03-01T15:31:41.579969Z', 6);
-INSERT INTO PROJECTAUTHORIZATION (role, project_id, user_name)
+INSERT INTO project_authorizations (role, project_id, user_id)
VALUES ('OWNER', 6, 'test_user_1');
-- --------------------------------------------------------------------------------
@@ -58,16 +58,16 @@ VALUES ('OWNER', 6, 'test_user_1');
-- --------------------------------------------------------------------------------
-- Add Portfolio to project 1
-INSERT INTO PORTFOLIO (name, number, project_id, targets, id)
+INSERT INTO portfolios (name, number, project_id, targets, id)
VALUES ('Test PortFolio Base', 1, 1, '{"metrics": [], "repeats":1}' FORMAT JSON, 1);
-INSERT INTO PORTFOLIO (name, number, project_id, targets, id)
+INSERT INTO portfolios (name, number, project_id, targets, id)
VALUES ('Test PortFolio Delete', 2, 1, '{"metrics": [], "repeats":1}' FORMAT JSON, 2);
-INSERT INTO PORTFOLIO (name, number, project_id, targets, id)
+INSERT INTO portfolios (name, number, project_id, targets, id)
VALUES ('Test PortFolio DeleteEditor', 3, 1, '{"metrics": [], "repeats":1}' FORMAT JSON, 3);
-UPDATE Project p
+UPDATE projects p
SET p.portfolios_created = 3, p.updated_at = '2024-03-01T15:31:41.579969Z'
WHERE p.id = 1;
@@ -75,19 +75,19 @@ WHERE p.id = 1;
-- Topologies
-- --------------------------------------------------------------------------------
-INSERT INTO TOPOLOGY (created_at, name, number, project_id, rooms, updated_at, id)
+INSERT INTO topologies (created_at, name, number, project_id, rooms, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Topology testUpdate', 1, 1, '[]' FORMAT JSON, '2024-03-01T15:31:41.579969Z', 1);
-INSERT INTO TOPOLOGY (created_at, name, number, project_id, rooms, updated_at, id)
+INSERT INTO topologies (created_at, name, number, project_id, rooms, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Topology testDeleteAsEditor', 2, 1, '[]' FORMAT JSON, '2024-03-01T15:31:41.579969Z', 2);
-INSERT INTO TOPOLOGY (created_at, name, number, project_id, rooms, updated_at, id)
+INSERT INTO topologies (created_at, name, number, project_id, rooms, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Topology testDelete', 3, 1, '[]' FORMAT JSON, '2024-03-01T15:31:41.579969Z', 3);
-INSERT INTO TOPOLOGY (created_at, name, number, project_id, rooms, updated_at, id)
+INSERT INTO topologies (created_at, name, number, project_id, rooms, updated_at, id)
VALUES ('2024-03-01T15:31:41.579969Z', 'Test Topology testDeleteUsed', 4, 1, '[]' FORMAT JSON, '2024-03-01T15:31:41.579969Z', 4);
-UPDATE Project p
+UPDATE projects p
SET p.topologies_created = 4, p.updated_at = '2024-03-01T15:31:41.579969Z'
WHERE p.id = 1;
@@ -95,21 +95,21 @@ WHERE p.id = 1;
-- Traces
-- --------------------------------------------------------------------------------
-INSERT INTO TRACE (id, name, type)
+INSERT INTO trace (id, name, type)
VALUES ('bitbrains-small', 'Bitbrains Small', 'small');
-- --------------------------------------------------------------------------------
-- Scenario
-- --------------------------------------------------------------------------------
-INSERT INTO SCENARIO (name, number, phenomena, portfolio_id, project_id, scheduler_name, topology_id, sampling_fraction, trace_id, id)
+INSERT INTO scenarios (name, number, phenomena, portfolio_id, project_id, scheduler_name, topology_id, sampling_fraction, trace_id, id)
VALUES ('Test Scenario testDelete', 1, '{"failures": false, "interference": false}' FORMAT JSON, 1, 1, 'test', 1, 1.0, 'bitbrains-small', 1);
-INSERT INTO SCENARIO (name, number, phenomena, portfolio_id, project_id, scheduler_name, topology_id, sampling_fraction, trace_id, id)
+INSERT INTO scenarios (name, number, phenomena, portfolio_id, project_id, scheduler_name, topology_id, sampling_fraction, trace_id, id)
VALUES ('Test Scenario testDeleteUsed', 2, '{"failures": false, "interference": false}' FORMAT JSON, 1, 1, 'test', 4, 1.0, 'bitbrains-small', 2);
-UPDATE Project p
+UPDATE projects p
SET p.scenarios_created = 2, p.updated_at = '2024-03-01T15:31:41.579969Z'
WHERE p.id = 1;
@@ -117,8 +117,8 @@ WHERE p.id = 1;
-- Job
-- --------------------------------------------------------------------------------
-INSERT INTO JOB (scenario_id, created_by, created_at, repeats, updated_at, state, runtime, results, id)
+INSERT INTO job (scenario_id, created_by, created_at, repeats, updated_at, state, runtime, results, id)
VALUES (1, 'test_user_1', '2024-03-01T15:31:41.579969Z', 1, '2024-03-01T15:31:41.579969Z', 'PENDING', 1, '{}' FORMAT JSON, 1);
-INSERT INTO JOB (scenario_id, created_by, created_at, repeats, updated_at, state, runtime, results, id)
+INSERT INTO job (scenario_id, created_by, created_at, repeats, updated_at, state, runtime, results, id)
VALUES (1, 'test_user_1', '2024-03-01T15:31:41.579969Z', 1, '2024-03-01T15:31:41.579969Z', 'PENDING', 1, '{}' FORMAT JSON, 2);
diff --git a/opendc-web/opendc-web-server/src/main/webui/.dockerignore b/opendc-web/opendc-web-server/src/main/webui/.dockerignore
new file mode 100644
index 00000000..b91894f6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/.dockerignore
@@ -0,0 +1,9 @@
+Dockerfile
+
+.idea/
+**/out
+*.iml
+.idea_modules/
+
+node_modules
+build
diff --git a/opendc-web/opendc-web-server/src/main/webui/.eslintrc b/opendc-web/opendc-web-server/src/main/webui/.eslintrc
new file mode 100644
index 00000000..1446fa02
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/.eslintrc
@@ -0,0 +1,16 @@
+{
+ "extends": ["next", "eslint:recommended"],
+ "env": {
+ "browser": true,
+ "node": true,
+ "es6": true
+ },
+ "overrides": [
+ {
+ "files": ["src/**/*.test.js"],
+ "env": {
+ "jest": true
+ }
+ }
+ ]
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/.gitignore b/opendc-web/opendc-web-server/src/main/webui/.gitignore
new file mode 100644
index 00000000..0f845719
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/.gitignore
@@ -0,0 +1,28 @@
+# Dependencies
+/node_modules
+
+# Testing
+/coverage
+
+# Production
+/build
+/public/__ENV.js
+
+# Misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# IntelliJ IDEA
+/.idea
+
+# Environment variables
+.env.local
+
+/.next
diff --git a/opendc-web/opendc-web-server/src/main/webui/.prettierrc.yaml b/opendc-web/opendc-web-server/src/main/webui/.prettierrc.yaml
new file mode 100644
index 00000000..9a2b9a95
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/.prettierrc.yaml
@@ -0,0 +1,5 @@
+trailingComma: "es5"
+tabWidth: 4
+semi: false
+singleQuote: true
+printWidth: 120
diff --git a/opendc-web/opendc-web-server/src/main/webui/Dockerfile b/opendc-web/opendc-web-server/src/main/webui/Dockerfile
new file mode 100644
index 00000000..6e30c96f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/Dockerfile
@@ -0,0 +1,27 @@
+FROM node:18-slim AS staging
+LABEL org.opencontainers.image.authors="OpenDC Maintainers <opendc@atlarge-research.com>"
+# Copy package details
+COPY ./package.json ./package-lock.json /opendc/
+RUN cd /opendc && npm ci
+
+# Build frontend
+FROM node:18-slim AS build
+
+COPY ./ /opendc
+COPY --from=staging /opendc/node_modules /opendc/node_modules
+RUN cd /opendc/ \
+ && npm run build \
+ && npm cache clean --force
+
+FROM node:18-slim
+COPY --from=build /opendc /opendc
+WORKDIR /opendc
+CMD npm run start
+
+LABEL org.opencontainers.image.authors="OpenDC Maintainers <opendc@atlarge-research.com>"
+LABEL org.opencontainers.image.url="https://opendc.org"
+LABEL org.opencontainers.image.documentation="https://opendc.org"
+LABEL org.opencontainers.image.source="https://github.com/atlarge-research/opendc"
+LABEL org.opencontainers.image.title="OpenDC Web UI"
+LABEL org.opencontainers.image.description="OpenDC Web UI Docker Image"
+LABEL org.opencontainers.image.vendor="AtLarge Research"
diff --git a/opendc-web/opendc-web-server/src/main/webui/README.md b/opendc-web/opendc-web-server/src/main/webui/README.md
new file mode 100644
index 00000000..24788e37
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/README.md
@@ -0,0 +1,106 @@
+# OpenDC Web UI
+
+The user-facing component of the OpenDC stack, allowing users to build and interact with their own (virtual)
+datacenters. Built in *React.js* and *Redux*, with the help of [Next.js](https://nextjs.org/).
+
+## Architecture
+
+The codebase follows a standard React.js structure, with static assets being contained in the `public` folder, while
+dynamic components and their styles are contained in `src`.
+
+### Pages
+
+All pages are represented by a component in the `pages` directory, following
+the [Next.js conventions](https://nextjs.org/docs/routing/introduction) for routing. There are components for the
+following pages:
+
+1. **index.js** - Entry page (`/`)
+2. **projects/index.js** - Overview of projects of the user (`/projects`)
+3. **projects/[project]/index.js** - Main application, with datacenter construction and simulation UI (`/projects/:projectId`
+and `/projects/:projectId/portfolios/:portfolioId`)
+4. **profile.js** - Profile of the current user (`/profile`)
+5. **404.js** - 404 page to appear when the route is invalid (`/*`)
+
+### Components & Containers
+
+The building blocks of the UI are divided into so-called *components* and *containers*
+([as encouraged](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) by the author of Redux).
+*Components* are considered 'pure', rendered as a function of input properties. *Containers*, on the other hand,
+are wrappers around *components*, injecting state through the properties of the components they wrap.
+
+Even the canvas (the main component of the app) is built using React components, with the help of the `react-konva`
+module. To illustrate: A rectangular object on the canvas is defined in a way that is not very different from how we
+define a standard `div` element on the splash page.
+
+### API Interaction
+
+The web-app needs to pull data in from the API of a backend running on a server. The functions that call routes are
+located in `src/api`. The actual logic responsible for calling these functions is contained in `src/data`.
+
+### State Management
+
+State for the topology editor is managed via a Redux store. State is kept there in an immutable form, only to be modified through
+actions being dispatched. These actions are contained in the `actions` folder, and the reducers (managing how state
+is updated according to dispatched actions) are located in `reducers`. If you're not familiar with the Redux
+approach to state management, have a look at their [official documentation](https://redux.js.org/).
+
+## Running the development server
+
+Before we can start the development server, create a file called `.env` in this directory and specify the base URL of
+the API that the React frontend will communicate with. For instance, if you run the OpenDC development server:
+
+```
+NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api
+```
+
+Now, you're ready to start the Next.js development server. Run the following command in the root of the repository
+(that is, two levels up where the `gradlew` file is located):
+
+```bash
+./gradlew :opendc-web:opendc-web-ui:nextDev
+```
+
+This will start a development server running on [`localhost:3000`](http://localhost:3000), watching for changes you make
+to the code and rebuilding automatically when you save changes.
+
+To compile everything for camera-ready deployment, use the following command:
+
+```bash
+./gradlew :opendc-web:opendc-web-ui:build
+```
+
+You can then run the production server using Next.js as follows:
+
+```bash
+./gradlew :opendc-web:opendc-web-ui:nextStart
+```
+
+## Tests
+
+Files containing tests can be recognized by the `.test.js` suffix. They are usually located right next to the source
+code they are testing, to make discovery easier.
+
+### Running all tests
+
+The following command runs all tests in the codebase using [Jest](https://jest.io). On top of this, it also watches the
+code for changes and reruns the tests whenever any file is saved.
+
+```bash
+./gradlew :opendc-web:opendc-web-ui:test
+```
+
+## Code Quality
+
+We use [Prettier](https://prettier.io) to ensure the formatting of the JavaScript codebase remains consistent. To format
+the files of the codebase according to the preferred coding style, run the following command:
+
+```bash
+./gradlew :opendc-web:opendc-web-ui:prettierFormat
+```
+
+Furthermore, we also employ [ESLint](https://eslint.org/) (via Next) to detect issues and problematic code in our
+codebase. To check for potential issues, run the following command:
+
+```bash
+./gradlew :opendc-web:opendc-web-ui:nextLint
+```
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java b/opendc-web/opendc-web-server/src/main/webui/api/index.js
index 345acdfe..3411b96e 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java
+++ b/opendc-web/opendc-web-server/src/main/webui/api/index.js
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 AtLarge Research
+ * Copyright (c) 2021 AtLarge Research
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -20,27 +20,37 @@
* SOFTWARE.
*/
-package org.opendc.web.server.rest.error;
-
-import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.ext.ExceptionMapper;
-import jakarta.ws.rs.ext.Provider;
-import org.opendc.web.proto.ProtocolError;
+import { apiUrl } from '../config'
/**
- * An [ExceptionMapper] for [MissingKotlinParameterException] thrown by Jackson.
+ * Send the specified request to the OpenDC API.
+ *
+ * @param auth The authentication context.
+ * @param path Relative path for the API.
+ * @param method The method to use for the request.
+ * @param body The body of the request.
*/
-@Provider
-public final class MissingKotlinParameterExceptionMapper implements ExceptionMapper<MissingKotlinParameterException> {
- @Override
- public Response toResponse(MissingKotlinParameterException exception) {
- return Response.status(Response.Status.BAD_REQUEST)
- .entity(new ProtocolError(
- Response.Status.BAD_REQUEST.getStatusCode(),
- "Field " + exception.getParameter().getName() + " is missing from body."))
- .type(MediaType.APPLICATION_JSON)
- .build();
+export async function request(auth, path, method = 'GET', body) {
+ const headers = {
+ 'Content-Type': 'application/json',
+ }
+
+ const { getAccessTokenSilently } = auth
+ if (getAccessTokenSilently) {
+ const token = await getAccessTokenSilently()
+ headers['Authorization'] = `Bearer ${token}`
}
+
+ const response = await fetch(`${apiUrl}/${path}`, {
+ method: method,
+ headers: headers,
+ body: body && JSON.stringify(body),
+ })
+ const json = await response.json()
+
+ if (!response.ok) {
+ throw json.message
+ }
+
+ return json
}
diff --git a/opendc-web/opendc-web-server/src/main/webui/api/portfolios.js b/opendc-web/opendc-web-server/src/main/webui/api/portfolios.js
new file mode 100644
index 00000000..d818876f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/api/portfolios.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { request } from './index'
+
+export function fetchPortfolio(auth, projectId, number) {
+ return request(auth, `projects/${projectId}/portfolios/${number}`)
+}
+
+export function fetchPortfolios(auth, projectId) {
+ return request(auth, `projects/${projectId}/portfolios`)
+}
+
+export function addPortfolio(auth, projectId, portfolio) {
+ return request(auth, `projects/${projectId}/portfolios`, 'POST', portfolio)
+}
+
+export function deletePortfolio(auth, projectId, number) {
+ return request(auth, `projects/${projectId}/portfolios/${number}`, 'DELETE')
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/api/projects.js b/opendc-web/opendc-web-server/src/main/webui/api/projects.js
new file mode 100644
index 00000000..e7e095da
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/api/projects.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { request } from './index'
+
+export function fetchProjects(auth) {
+ return request(auth, `projects/`)
+}
+
+export function fetchProject(auth, projectId) {
+ return request(auth, `projects/${projectId}`)
+}
+
+export function addProject(auth, project) {
+ return request(auth, 'projects/', 'POST', project)
+}
+
+export function deleteProject(auth, projectId) {
+ return request(auth, `projects/${projectId}`, 'DELETE')
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/api/scenarios.js b/opendc-web/opendc-web-server/src/main/webui/api/scenarios.js
new file mode 100644
index 00000000..7eeb8f28
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/api/scenarios.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { request } from './index'
+
+export function fetchScenario(auth, projectId, scenarioId) {
+ return request(auth, `projects/${projectId}/scenarios/${scenarioId}`)
+}
+
+export function fetchScenariosOfPortfolio(auth, projectId, portfolioId) {
+ return request(auth, `projects/${projectId}/portfolios/${portfolioId}/scenarios`)
+}
+
+export function addScenario(auth, projectId, portfolioId, scenario) {
+ return request(auth, `projects/${projectId}/portfolios/${portfolioId}/scenarios`, 'POST', scenario)
+}
+
+export function deleteScenario(auth, projectId, scenarioId) {
+ return request(auth, `projects/${projectId}/scenarios/${scenarioId}`, 'DELETE')
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/api/schedulers.js b/opendc-web/opendc-web-server/src/main/webui/api/schedulers.js
new file mode 100644
index 00000000..0b8b8153
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/api/schedulers.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { request } from './index'
+
+export function fetchSchedulers(auth) {
+ return request(auth, 'schedulers/')
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/api/topologies.js b/opendc-web/opendc-web-server/src/main/webui/api/topologies.js
new file mode 100644
index 00000000..0509c6d0
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/api/topologies.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { request } from './index'
+
+export function fetchTopology(auth, projectId, number) {
+ return request(auth, `projects/${projectId}/topologies/${number}`)
+}
+
+export function fetchTopologies(auth, projectId) {
+ return request(auth, `projects/${projectId}/topologies`)
+}
+
+export function addTopology(auth, projectId, topology) {
+ return request(auth, `projects/${projectId}/topologies`, 'POST', topology)
+}
+
+export function updateTopology(auth, topology) {
+ const { project, number, rooms } = topology
+ return request(auth, `projects/${project.id}/topologies/${number}`, 'PUT', { rooms })
+}
+
+export function deleteTopology(auth, projectId, number) {
+ return request(auth, `projects/${projectId}/topologies/${number}`, 'DELETE')
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/api/traces.js b/opendc-web/opendc-web-server/src/main/webui/api/traces.js
new file mode 100644
index 00000000..fd637ac3
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/api/traces.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { request } from './index'
+
+export function fetchTraces(auth) {
+ return request(auth, 'traces/')
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/api/users.js b/opendc-web/opendc-web-server/src/main/webui/api/users.js
new file mode 100644
index 00000000..12a9be05
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/api/users.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { request } from './index'
+
+/**
+ * Fetch information about the user from the web server.
+ *
+ * @param auth The authentication object.
+ */
+export function fetchUser(auth) {
+ return request(auth, `users/me`)
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/auth.js b/opendc-web/opendc-web-server/src/main/webui/auth.js
new file mode 100644
index 00000000..8c88f526
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/auth.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { Auth0Provider, useAuth0 } from '@auth0/auth0-react'
+import { useEffect } from 'react'
+import { auth } from './config'
+
+/**
+ * Helper function to provide the authentication context in case Auth0 is not
+ * configured and the user is anonymous.
+ */
+function useAnonymousAuth() {
+ return {
+ isAnonymous: true,
+ isAuthenticated: false,
+ isLoading: false,
+ logout: () => {},
+ loginWithRedirect: () => {},
+ }
+}
+
+/**
+ * Determine whether the auth domain is anonymous.
+ */
+function isAnonymousDomain(config) {
+ return !config.domain || config.domain === '%%NEXT_PUBLIC_AUTH0_DOMAIN%%'
+}
+
+/**
+ * Force the user to be authenticated or redirect to the homepage.
+ */
+function useRequireAuth0() {
+ const auth = useAuth0()
+ const { loginWithRedirect, isLoading, isAuthenticated } = auth
+
+ useEffect(() => {
+ if (!isLoading && !isAuthenticated) {
+ loginWithRedirect()
+ }
+ }, [loginWithRedirect, isLoading, isAuthenticated])
+}
+
+/**
+ * Obtain the authentication context.
+ */
+export const useAuth = isAnonymousDomain(auth) ? useAnonymousAuth : useAuth0
+
+/**
+ * Force the user to be authenticated or redirect to the homepage.
+ */
+export const useRequireAuth = isAnonymousDomain(auth) ? () => {} : useRequireAuth0
+
+/**
+ * AuthProvider which provides an authentication context.
+ */
+export function AuthProvider({ children }) {
+ const authConfig = auth
+
+ if (!isAnonymousDomain(authConfig)) {
+ return (
+ <Auth0Provider
+ domain={authConfig.domain}
+ clientId={authConfig.clientId}
+ redirectUri={authConfig.redirectUri}
+ audience={authConfig.audience}
+ >
+ {children}
+ </Auth0Provider>
+ )
+ }
+
+ return children
+}
+
+AuthProvider.propTypes = {
+ children: PropTypes.node,
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/AppHeader.js b/opendc-web/opendc-web-server/src/main/webui/components/AppHeader.js
new file mode 100644
index 00000000..514dce2a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/AppHeader.js
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import Image from 'next/image'
+import PropTypes from 'prop-types'
+import React from 'react'
+import {
+ Masthead,
+ MastheadMain,
+ MastheadBrand,
+ MastheadContent,
+ Toolbar,
+ ToolbarContent,
+ ToolbarItem,
+} from '@patternfly/react-core'
+import Link from 'next/link'
+import AppHeaderTools from './AppHeaderTools'
+import AppHeaderUser from './AppHeaderUser'
+import ProjectSelector from './context/ProjectSelector'
+
+import styles from './AppHeader.module.css'
+
+export default function AppHeader({ nav }) {
+ return (
+ <Masthead id="app-header" className={styles.header}>
+ <MastheadMain>
+ <MastheadBrand className={styles.logo} component={(props) => <Link href="/projects" {...props} />}>
+ <Image src="/img/logo.svg" alt="OpenDC logo" width={25} height={25} />
+ <span>OpenDC</span>
+ </MastheadBrand>
+ </MastheadMain>
+ <MastheadContent>
+ <Toolbar id="toolbar" isFullHeight isStatic>
+ <ToolbarContent>
+ <ToolbarItem>
+ <ProjectSelector />
+ </ToolbarItem>
+ {nav && <ToolbarItem>{nav}</ToolbarItem>}
+ <AppHeaderTools />
+ <AppHeaderUser />
+ </ToolbarContent>
+ </Toolbar>
+ </MastheadContent>
+ </Masthead>
+ )
+}
+
+AppHeader.propTypes = {
+ nav: PropTypes.node,
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/AppHeader.module.css b/opendc-web/opendc-web-server/src/main/webui/components/AppHeader.module.css
new file mode 100644
index 00000000..9d5dbed1
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/AppHeader.module.css
@@ -0,0 +1,42 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.header.header {
+ /* Increase precedence */
+ --pf-c-masthead--m-display-inline__content--MinHeight: 3rem;
+ --pf-c-masthead--m-display-inline__main--MinHeight: 3rem;
+
+ --pf-c-masthead--c-context-selector--Width: 200px;
+}
+
+.logo > span {
+ margin-left: 8px;
+ color: #fff;
+ align-self: center;
+ font-weight: 600;
+ font-size: 0.9rem;
+}
+
+.logo:hover,
+.logo:focus > span {
+ --pf-global--link--TextDecoration: none;
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/AppHeaderTools.js b/opendc-web/opendc-web-server/src/main/webui/components/AppHeaderTools.js
new file mode 100644
index 00000000..499bceef
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/AppHeaderTools.js
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import {
+ Button,
+ ButtonVariant,
+ Dropdown,
+ DropdownItem,
+ KebabToggle,
+ ToolbarGroup,
+ ToolbarItem,
+} from '@patternfly/react-core'
+import { useReducer } from 'react'
+import { GithubIcon, HelpIcon } from '@patternfly/react-icons'
+
+function AppHeaderTools() {
+ const [isKebabDropdownOpen, toggleKebabDropdown] = useReducer((t) => !t, false)
+ const kebabDropdownItems = [
+ <DropdownItem
+ key={0}
+ component={
+ <a href="https://opendc.org" target="_blank" rel="noreferrer">
+ <HelpIcon /> Help
+ </a>
+ }
+ />,
+ ]
+
+ return (
+ <ToolbarGroup
+ variant="icon-button-group"
+ alignment={{ default: 'alignRight' }}
+ spacer={{ default: 'spacerNone', md: 'spacerMd' }}
+ >
+ <ToolbarGroup variant="icon-button-group" visibility={{ default: 'hidden', lg: 'visible' }}>
+ <ToolbarItem>
+ <Button
+ component="a"
+ href="https://github.com/atlarge-research/opendc"
+ target="_blank"
+ aria-label="Source code"
+ variant={ButtonVariant.plain}
+ >
+ <GithubIcon />
+ </Button>
+ </ToolbarItem>
+ <ToolbarItem>
+ <Button
+ component="a"
+ href="https://opendc.org/"
+ target="_blank"
+ aria-label="Help actions"
+ variant={ButtonVariant.plain}
+ >
+ <HelpIcon />
+ </Button>
+ </ToolbarItem>
+ </ToolbarGroup>
+ <ToolbarItem visibility={{ lg: 'hidden' }}>
+ <Dropdown
+ isPlain
+ position="right"
+ toggle={<KebabToggle onToggle={toggleKebabDropdown} />}
+ isOpen={isKebabDropdownOpen}
+ dropdownItems={kebabDropdownItems}
+ />
+ </ToolbarItem>
+ </ToolbarGroup>
+ )
+}
+
+AppHeaderTools.propTypes = {}
+
+export default AppHeaderTools
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/AppHeaderUser.js b/opendc-web/opendc-web-server/src/main/webui/components/AppHeaderUser.js
new file mode 100644
index 00000000..3a73d9ba
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/AppHeaderUser.js
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import {
+ Dropdown,
+ DropdownToggle,
+ Skeleton,
+ ToolbarItem,
+ DropdownItem,
+ DropdownGroup,
+ Avatar,
+ Progress,
+ ProgressSize,
+ DropdownSeparator,
+} from '@patternfly/react-core'
+import { useReducer } from 'react'
+import { useAuth } from '../auth'
+import useUser from '../data/user'
+
+export default function AppHeaderUser() {
+ const { logout, user, isAuthenticated, isLoading } = useAuth()
+ const username = isAuthenticated || isLoading ? user?.name : 'Anonymous'
+ const avatar = isAuthenticated || isLoading ? user?.picture : '/img/avatar.svg'
+
+ const { data } = useUser()
+ const simulationBudget = data?.accounting?.simulationTimeBudget ?? 3600
+ const simulationTime = data?.accounting?.simulationTime | 0
+
+ const [isDropdownOpen, toggleDropdown] = useReducer((t) => !t, false)
+ const userDropdownItems = [
+ <DropdownGroup key="budget" label="Monthly Simulation Budget">
+ <DropdownItem isDisabled>
+ <Progress
+ min={0}
+ max={simulationBudget}
+ value={simulationTime}
+ title={`${Math.ceil(simulationTime / 60)} of ${Math.ceil(simulationBudget / 60)} minutes`}
+ size={ProgressSize.sm}
+ />
+ </DropdownItem>
+ </DropdownGroup>,
+ <DropdownSeparator key="separator" />,
+ <DropdownItem
+ key="group 2 logout"
+ isDisabled={!isAuthenticated}
+ onClick={() => logout({ returnTo: window.location.origin })}
+ >
+ Logout
+ </DropdownItem>,
+ ]
+
+ const avatarComponent = avatar ? (
+ <Avatar src={avatar} alt="Avatar image" size="sm" />
+ ) : (
+ <Skeleton className="pf-c-avatar" shape="circle" width="24px" screenreaderText="Loading avatar" />
+ )
+
+ return (
+ <ToolbarItem visibility={{ default: 'hidden', md: 'visible' }}>
+ <Dropdown
+ isFullHeight
+ position="right"
+ isOpen={isDropdownOpen}
+ toggle={
+ <DropdownToggle onToggle={toggleDropdown} icon={avatarComponent}>
+ {username ?? (
+ <Skeleton
+ fontSize="xs"
+ width="150px"
+ className="pf-u-display-inline-flex"
+ screenreaderText="Loading username"
+ />
+ )}
+ </DropdownToggle>
+ }
+ dropdownItems={userDropdownItems}
+ />
+ </ToolbarItem>
+ )
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/AppPage.js b/opendc-web/opendc-web-server/src/main/webui/components/AppPage.js
new file mode 100644
index 00000000..2893146e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/AppPage.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import AppHeader from './AppHeader'
+import React from 'react'
+import { Page, PageGroup, PageBreadcrumb } from '@patternfly/react-core'
+
+export function AppPage({ children, breadcrumb, contextSelectors }) {
+ return (
+ <Page header={<AppHeader />}>
+ <PageGroup>
+ {contextSelectors}
+ {breadcrumb && <PageBreadcrumb>{breadcrumb}</PageBreadcrumb>}
+ </PageGroup>
+ {children}
+ </Page>
+ )
+}
+
+AppPage.propTypes = {
+ breadcrumb: PropTypes.node,
+ contextSelectors: PropTypes.node,
+ children: PropTypes.node,
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelectionSection.js b/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelectionSection.js
new file mode 100644
index 00000000..f3c25b79
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelectionSection.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { contextSelectionSection } from './ContextSelectionSection.module.css'
+
+function ContextSelectionSection({ children }) {
+ return <section className={contextSelectionSection}>{children}</section>
+}
+
+ContextSelectionSection.propTypes = {
+ children: PropTypes.node,
+}
+
+export default ContextSelectionSection
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelectionSection.module.css b/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelectionSection.module.css
new file mode 100644
index 00000000..0e902af0
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelectionSection.module.css
@@ -0,0 +1,28 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.contextSelectionSection {
+ padding-left: var(--pf-c-page__main-breadcrumb--PaddingLeft);
+ flex-shrink: 0;
+ border-bottom: var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--100);
+ background-color: var(--pf-c-page__main-breadcrumb--BackgroundColor);
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelector.js b/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelector.js
new file mode 100644
index 00000000..d2601008
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelector.js
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { ContextSelector as PFContextSelector, ContextSelectorItem } from '@patternfly/react-core'
+import { useMemo, useState } from 'react'
+
+import styles from './ContextSelector.module.css'
+
+function ContextSelector({ id, type = 'page', toggleText, items, onSelect, onToggle, isOpen, isFullHeight }) {
+ const [searchValue, setSearchValue] = useState('')
+ const filteredItems = useMemo(
+ () => items.filter(({ name }) => name.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1) || items,
+ [items, searchValue]
+ )
+
+ return (
+ <PFContextSelector
+ id={id}
+ className={type === 'page' && styles.pageSelector}
+ toggleText={toggleText}
+ onSearchInputChange={(value) => setSearchValue(value)}
+ searchInputValue={searchValue}
+ onToggle={(_, isOpen) => onToggle(isOpen)}
+ onSelect={(event) => {
+ const targetId = +event.target.value
+ const target = items.find((item) => item.id === targetId)
+
+ onSelect(target)
+ onToggle(!isOpen)
+ }}
+ isOpen={isOpen}
+ isFullHeight={isFullHeight}
+ >
+ {filteredItems.map((item) => (
+ <ContextSelectorItem key={item.id} value={item.id}>
+ {item.name}
+ </ContextSelectorItem>
+ ))}
+ </PFContextSelector>
+ )
+}
+
+const Item = PropTypes.shape({
+ id: PropTypes.any.isRequired,
+ name: PropTypes.string.isRequired,
+})
+
+ContextSelector.propTypes = {
+ id: PropTypes.string,
+ type: PropTypes.oneOf(['app', 'page']),
+ items: PropTypes.arrayOf(Item).isRequired,
+ toggleText: PropTypes.string,
+ onSelect: PropTypes.func.isRequired,
+ onToggle: PropTypes.func.isRequired,
+ isOpen: PropTypes.bool,
+ isFullHeight: PropTypes.bool,
+}
+
+export default ContextSelector
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelector.module.css b/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelector.module.css
new file mode 100644
index 00000000..7662d00c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/context/ContextSelector.module.css
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.pageSelector.pageSelector {
+ /* Ensure this selector has precedence over the default one */
+ margin-right: 20px;
+
+ --pf-c-context-selector__menu--ZIndex: var(--pf-global--ZIndex--lg);
+ --pf-c-context-selector__toggle--PaddingTop: var(--pf-global--spacer--sm);
+ --pf-c-context-selector__toggle--PaddingRight: 0;
+ --pf-c-context-selector__toggle--PaddingBottom: var(--pf-global--spacer--sm);
+ --pf-c-context-selector__toggle--PaddingLeft: 0;
+ --pf-c-context-selector__toggle--BorderWidth: 0;
+ --pf-c-context-selector__toggle-text--FontSize: var(--pf-global--FontSize--sm);
+}
+
+.pageSelector.pageSelector :global(.pf-c-context-selector__toggle):active,
+.pageSelector.pageSelector :global(.pf-c-context-selector__toggle):focus-within,
+.pageSelector.pageSelector :global(.pf-c-context-selector__toggle):global(.pf-m-active) {
+ --pf-c-context-selector__toggle--after--BorderBottomWidth: 0;
+}
+
+.pageSelector.pageSelector:global(.pf-m-expanded) > :global(.pf-c-context-selector__toggle) {
+ --pf-c-context-selector__toggle--after--BorderBottomWidth: 0;
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/context/PortfolioSelector.js b/opendc-web/opendc-web-server/src/main/webui/components/context/PortfolioSelector.js
new file mode 100644
index 00000000..e401e6fc
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/context/PortfolioSelector.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+import { usePortfolios } from '../../data/project'
+import { Portfolio } from '../../shapes'
+import ContextSelector from './ContextSelector'
+
+function PortfolioSelector({ activePortfolio }) {
+ const router = useRouter()
+
+ const [isOpen, setOpen] = useState(false)
+ const { data: portfolios = [] } = usePortfolios(activePortfolio?.project?.id, { enabled: isOpen })
+
+ return (
+ <ContextSelector
+ id="portfolio"
+ toggleText={activePortfolio ? `Portfolio: ${activePortfolio.name}` : 'Select portfolio'}
+ activeItem={activePortfolio}
+ items={portfolios}
+ onSelect={(portfolio) => router.push(`/projects/${portfolio.project.id}/portfolios/${portfolio.number}`)}
+ onToggle={setOpen}
+ isOpen={isOpen}
+ />
+ )
+}
+
+PortfolioSelector.propTypes = {
+ activePortfolio: Portfolio,
+}
+
+export default PortfolioSelector
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/context/ProjectSelector.js b/opendc-web/opendc-web-server/src/main/webui/components/context/ProjectSelector.js
new file mode 100644
index 00000000..f2791b38
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/context/ProjectSelector.js
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+import { useProjects, useProject } from '../../data/project'
+import { Project } from '../../shapes'
+import ContextSelector from './ContextSelector'
+
+function ProjectSelector() {
+ const router = useRouter()
+ const projectId = +router.query['project']
+
+ const [isOpen, setOpen] = useState(false)
+ const { data: activeProject } = useProject(+projectId)
+ const { data: projects = [] } = useProjects({ enabled: isOpen })
+
+ return (
+ <ContextSelector
+ id="project"
+ type="app"
+ toggleText={activeProject ? activeProject.name : 'Select project'}
+ items={projects}
+ onSelect={(project) => router.push(`/projects/${project.id}`)}
+ onToggle={setOpen}
+ isOpen={isOpen}
+ isFullHeight
+ />
+ )
+}
+
+ProjectSelector.propTypes = {
+ activeProject: Project,
+}
+
+export default ProjectSelector
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/context/TopologySelector.js b/opendc-web/opendc-web-server/src/main/webui/components/context/TopologySelector.js
new file mode 100644
index 00000000..355d9f4b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/context/TopologySelector.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+import { useTopologies } from '../../data/topology'
+import { Topology } from '../../shapes'
+import ContextSelector from './ContextSelector'
+
+function TopologySelector({ activeTopology }) {
+ const router = useRouter()
+
+ const [isOpen, setOpen] = useState(false)
+ const { data: topologies = [] } = useTopologies(activeTopology?.project?.id, { enabled: isOpen })
+
+ return (
+ <ContextSelector
+ id="topology"
+ toggleText={activeTopology ? `Topology: ${activeTopology.name}` : 'Select topology'}
+ activeItem={activeTopology}
+ items={topologies}
+ onSelect={(topology) => router.push(`/projects/${topology.project.id}/topologies/${topology.number}`)}
+ onToggle={setOpen}
+ isOpen={isOpen}
+ />
+ )
+}
+
+TopologySelector.propTypes = {
+ activeTopology: Topology,
+}
+
+export default TopologySelector
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenario.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenario.js
new file mode 100644
index 00000000..fd9a72d2
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenario.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { PlusIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
+import { useState } from 'react'
+import { useNewScenario } from '../../data/project'
+import NewScenarioModal from './NewScenarioModal'
+
+function NewScenario({ projectId, portfolioId }) {
+ const [isVisible, setVisible] = useState(false)
+ const { mutate: addScenario } = useNewScenario()
+
+ const onSubmit = (projectId, portfolioNumber, data) => {
+ addScenario({ projectId, portfolioNumber, data })
+ setVisible(false)
+ }
+
+ return (
+ <>
+ <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}>
+ New Scenario
+ </Button>
+ <NewScenarioModal
+ projectId={projectId}
+ portfolioId={portfolioId}
+ isOpen={isVisible}
+ onSubmit={onSubmit}
+ onCancel={() => setVisible(false)}
+ />
+ </>
+ )
+}
+
+NewScenario.propTypes = {
+ projectId: PropTypes.number,
+ portfolioId: PropTypes.number,
+}
+
+export default NewScenario
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenarioModal.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenarioModal.js
new file mode 100644
index 00000000..ed35c163
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenarioModal.js
@@ -0,0 +1,157 @@
+import PropTypes from 'prop-types'
+import React, { useRef, useState } from 'react'
+import Modal from '../util/modals/Modal'
+import {
+ Checkbox,
+ Form,
+ FormGroup,
+ FormSection,
+ FormSelect,
+ FormSelectOption,
+ NumberInput,
+ TextInput,
+} from '@patternfly/react-core'
+import { useSchedulers, useTraces } from '../../data/experiments'
+import { useTopologies } from '../../data/topology'
+import { usePortfolio } from '../../data/project'
+
+function NewScenarioModal({ projectId, portfolioId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) {
+ const { data: portfolio } = usePortfolio(projectId, portfolioId)
+ const { data: topologies = [] } = useTopologies(projectId, { enabled: isOpen })
+ const { data: traces = [] } = useTraces({ enabled: isOpen })
+ const { data: schedulers = [] } = useSchedulers({ enabled: isOpen })
+
+ // eslint-disable-next-line no-unused-vars
+ const [isSubmitted, setSubmitted] = useState(false)
+ const [traceLoad, setTraceLoad] = useState(100)
+ const [trace, setTrace] = useState(undefined)
+ const [topology, setTopology] = useState(undefined)
+ const [scheduler, setScheduler] = useState(undefined)
+ const [failuresEnabled, setFailuresEnabled] = useState(false)
+ const [opPhenEnabled, setOpPhenEnabled] = useState(false)
+ const nameInput = useRef(null)
+
+ const resetState = () => {
+ setSubmitted(false)
+ setTraceLoad(100)
+ setTrace(undefined)
+ setTopology(undefined)
+ setScheduler(undefined)
+ setFailuresEnabled(false)
+ setOpPhenEnabled(false)
+ nameInput.current.value = ''
+ }
+
+ const onSubmit = (event) => {
+ setSubmitted(true)
+
+ if (event) {
+ event.preventDefault()
+ }
+
+ const name = nameInput.current.value
+
+ onSubmitUpstream(portfolio.project.id, portfolio.number, {
+ name,
+ workload: {
+ trace: trace || traces[0].id,
+ samplingFraction: traceLoad / 100,
+ },
+ topology: topology || topologies[0].number,
+ phenomena: {
+ failures: failuresEnabled,
+ interference: opPhenEnabled,
+ },
+ schedulerName: scheduler || schedulers[0],
+ })
+
+ resetState()
+ return true
+ }
+ const onCancel = () => {
+ onCancelUpstream()
+ resetState()
+ }
+
+ return (
+ <Modal title="New Scenario" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form onSubmit={onSubmit}>
+ <FormGroup label="Name" fieldId="name" isRequired>
+ <TextInput
+ id="name"
+ name="name"
+ type="text"
+ isDisabled={portfolio?.scenarios?.length === 0}
+ defaultValue={portfolio?.scenarios?.length === 0 ? 'Base scenario' : ''}
+ ref={nameInput}
+ />
+ </FormGroup>
+ <FormSection title="Workload">
+ <FormGroup label="Trace" fieldId="trace" isRequired>
+ <FormSelect id="trace" name="trace" value={trace} onChange={setTrace}>
+ {traces.map((trace) => (
+ <FormSelectOption value={trace.id} key={trace.id} label={trace.name} />
+ ))}
+ </FormSelect>
+ </FormGroup>
+ <FormGroup label="Load Sampling Fraction" fieldId="trace-load" isRequired>
+ <NumberInput
+ name="trace-load"
+ type="number"
+ min={0}
+ max={100}
+ value={traceLoad}
+ onMinus={() => setTraceLoad((load) => load - 1)}
+ onPlus={() => setTraceLoad((load) => load + 1)}
+ onChange={(e) => setTraceLoad(Number(e.target.value))}
+ unit="%"
+ />
+ </FormGroup>
+ </FormSection>
+ <FormSection title="Topology">
+ <FormGroup label="Topology" fieldId="topology" isRequired>
+ <FormSelect id="topology" name="topology" value={topology} onChange={setTopology}>
+ {topologies.map((topology) => (
+ <FormSelectOption value={topology.number} key={topology.number} label={topology.name} />
+ ))}
+ </FormSelect>
+ </FormGroup>
+
+ <FormGroup label="Scheduler" fieldId="scheduler" isRequired>
+ <FormSelect id="scheduler" name="scheduler" value={scheduler} onChange={setScheduler}>
+ {schedulers.map((scheduler) => (
+ <FormSelectOption value={scheduler} key={scheduler} label={scheduler} />
+ ))}
+ </FormSelect>
+ </FormGroup>
+ </FormSection>
+ <FormSection title="Operational Phenomena">
+ <Checkbox
+ label="Failures"
+ id="failures"
+ name="failures"
+ isChecked={failuresEnabled}
+ onChange={() => setFailuresEnabled((e) => !e)}
+ />
+ <Checkbox
+ label="Performance Interference"
+ id="perf-interference"
+ name="perf-interference"
+ isChecked={opPhenEnabled}
+ onChange={() => setOpPhenEnabled((e) => !e)}
+ />
+ </FormSection>
+ </Form>
+ </Modal>
+ )
+}
+
+NewScenarioModal.propTypes = {
+ projectId: PropTypes.number,
+ portfolioId: PropTypes.number,
+ isOpen: PropTypes.bool.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+}
+
+export default NewScenarioModal
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioOverview.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioOverview.js
new file mode 100644
index 00000000..e561b655
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioOverview.js
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import {
+ Card,
+ CardActions,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ Chip,
+ ChipGroup,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Grid,
+ GridItem,
+ Skeleton,
+} from '@patternfly/react-core'
+import React from 'react'
+import { usePortfolio } from '../../data/project'
+import { METRIC_NAMES } from '../../util/available-metrics'
+import NewScenario from './NewScenario'
+import ScenarioTable from './ScenarioTable'
+
+function PortfolioOverview({ projectId, portfolioId }) {
+ const { status, data: portfolio } = usePortfolio(projectId, portfolioId)
+
+ return (
+ <Grid hasGutter>
+ <GridItem md={2}>
+ <Card>
+ <CardTitle>Details</CardTitle>
+ <CardBody>
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Name</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.name ?? <Skeleton screenreaderText="Loading portfolio" />}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Scenarios</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.scenarios?.length ?? <Skeleton screenreaderText="Loading portfolio" />}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Metrics</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio ? (
+ portfolio.targets.metrics.length > 0 ? (
+ <ChipGroup>
+ {portfolio.targets.metrics.map((metric) => (
+ <Chip isReadOnly key={metric}>
+ {METRIC_NAMES[metric]}
+ </Chip>
+ ))}
+ </ChipGroup>
+ ) : (
+ 'No metrics enabled'
+ )
+ ) : (
+ <Skeleton screenreaderText="Loading portfolio" />
+ )}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Repeats per Scenario</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.targets?.repeats ?? <Skeleton screenreaderText="Loading portfolio" />}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={6}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <NewScenario projectId={projectId} portfolioId={portfolioId} />
+ </CardActions>
+ <CardTitle>Scenarios</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <ScenarioTable portfolio={portfolio} status={status} />
+ </CardBody>
+ </Card>
+ </GridItem>
+ </Grid>
+ )
+}
+
+PortfolioOverview.propTypes = {
+ projectId: PropTypes.number,
+ portfolioId: PropTypes.number,
+}
+
+export default PortfolioOverview
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResultInfo.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResultInfo.js
new file mode 100644
index 00000000..dbfa928f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResultInfo.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { Tooltip } from '@patternfly/react-core'
+import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'
+import { METRIC_DESCRIPTIONS } from '../../util/available-metrics'
+
+function PortfolioResultInfo({ metric }) {
+ return (
+ <Tooltip position="top" content={<div>{METRIC_DESCRIPTIONS[metric]}</div>}>
+ <OutlinedQuestionCircleIcon title="Metric information" />
+ </Tooltip>
+ )
+}
+
+PortfolioResultInfo.propTypes = {
+ metric: PropTypes.string.isRequired,
+}
+
+export default PortfolioResultInfo
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResults.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResults.js
new file mode 100644
index 00000000..62150fa7
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResults.js
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { mean, std } from 'mathjs'
+import React, { useMemo } from 'react'
+import PropTypes from 'prop-types'
+import { VictoryErrorBar } from 'victory-errorbar'
+import { METRIC_NAMES, METRIC_UNITS, AVAILABLE_METRICS } from '../../util/available-metrics'
+import {
+ Bullseye,
+ Card,
+ CardActions,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ Grid,
+ GridItem,
+ Spinner,
+ Title,
+} from '@patternfly/react-core'
+import { Chart, ChartAxis, ChartBar, ChartTooltip } from '@patternfly/react-charts'
+import { ErrorCircleOIcon, CubesIcon } from '@patternfly/react-icons'
+import { usePortfolio } from '../../data/project'
+import PortfolioResultInfo from './PortfolioResultInfo'
+import NewScenario from './NewScenario'
+
+function PortfolioResults({ projectId, portfolioId }) {
+ const { status, data: portfolio } = usePortfolio(projectId, portfolioId)
+ const scenarios = useMemo(() => portfolio?.scenarios ?? [], [portfolio])
+
+ const label = ({ datum }) =>
+ `${datum.x}: ${datum.y.toLocaleString()} ± ${datum.errorY.toLocaleString()} ${METRIC_UNITS[datum.metric]}`
+ const selectedMetrics = new Set(portfolio?.targets?.metrics ?? [])
+ const dataPerMetric = useMemo(() => {
+ const dataPerMetric = {}
+ AVAILABLE_METRICS.forEach((metric) => {
+ dataPerMetric[metric] = scenarios
+ .filter((scenario) => scenario.jobs && scenario.jobs[scenario.jobs.length - 1].results)
+ .map((scenario) => {
+ const job = scenario.jobs[scenario.jobs.length - 1]
+ return {
+ metric,
+ x: scenario.name,
+ y: mean(job.results[metric]),
+ errorY: std(job.results[metric]),
+ label,
+ }
+ })
+ })
+ return dataPerMetric
+ }, [scenarios])
+
+ const categories = useMemo(() => ({ x: scenarios.map((s) => s.name).reverse() }), [scenarios])
+
+ if (status === 'loading') {
+ return (
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={Spinner} />
+ <Title size="lg" headingLevel="h4">
+ Loading Results
+ </Title>
+ </EmptyState>
+ </Bullseye>
+ )
+ } else if (status === 'error') {
+ return (
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={ErrorCircleOIcon} />
+ <Title size="lg" headingLevel="h4">
+ Unable to connect
+ </Title>
+ <EmptyStateBody>
+ There was an error retrieving data. Check your connection and try again.
+ </EmptyStateBody>
+ </EmptyState>
+ </Bullseye>
+ )
+ } else if (scenarios.length === 0) {
+ return (
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={CubesIcon} />
+ <Title size="lg" headingLevel="h4">
+ No results
+ </Title>
+ <EmptyStateBody>
+ No results are currently available for this portfolio. Run a scenario to obtain simulation
+ results.
+ </EmptyStateBody>
+ <NewScenario projectId={projectId} portfolioId={portfolioId} />
+ </EmptyState>
+ </Bullseye>
+ )
+ }
+
+ return (
+ <Grid hasGutter>
+ {AVAILABLE_METRICS.map(
+ (metric) =>
+ selectedMetrics.has(metric) && (
+ <GridItem xl={6} lg={12} key={metric}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <PortfolioResultInfo metric={metric} />
+ </CardActions>
+ <CardTitle>{METRIC_NAMES[metric]}</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <Chart
+ width={650}
+ height={250}
+ padding={{
+ top: 10,
+ bottom: 60,
+ left: 130,
+ }}
+ domainPadding={25}
+ >
+ <ChartAxis />
+ <ChartAxis
+ dependentAxis
+ showGrid
+ label={METRIC_UNITS[metric]}
+ fixLabelOverlap
+ />
+ <ChartBar
+ categories={categories}
+ data={dataPerMetric[metric]}
+ labelComponent={<ChartTooltip constrainToVisibleArea />}
+ barWidth={25}
+ horizontal
+ />
+ <VictoryErrorBar
+ categories={categories}
+ data={dataPerMetric[metric]}
+ errorY={(d) => d.errorY}
+ labelComponent={<></>}
+ horizontal
+ />
+ </Chart>
+ </CardBody>
+ </Card>
+ </GridItem>
+ )
+ )}
+ </Grid>
+ )
+}
+
+PortfolioResults.propTypes = {
+ projectId: PropTypes.number,
+ portfolioId: PropTypes.number,
+}
+
+export default PortfolioResults
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioState.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioState.js
new file mode 100644
index 00000000..99d83f64
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioState.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { ClockIcon, CheckCircleIcon, ErrorCircleOIcon } from '@patternfly/react-icons'
+import { JobState } from '../../shapes'
+
+function ScenarioState({ state }) {
+ switch (state) {
+ case 'PENDING':
+ case 'CLAIMED':
+ return (
+ <span>
+ <ClockIcon color="blue" /> Queued
+ </span>
+ )
+ case 'RUNNING':
+ return (
+ <span>
+ <ClockIcon color="green" /> Running
+ </span>
+ )
+ case 'FINISHED':
+ return (
+ <span>
+ <CheckCircleIcon color="green" /> Finished
+ </span>
+ )
+ case 'FAILED':
+ return (
+ <span>
+ <ErrorCircleOIcon color="red" /> Failed
+ </span>
+ )
+ }
+
+ return 'Unknown'
+}
+
+ScenarioState.propTypes = {
+ state: JobState.isRequired,
+}
+
+export default ScenarioState
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js
new file mode 100644
index 00000000..b068d045
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Bullseye } from '@patternfly/react-core'
+import Link from 'next/link'
+import { TableComposable, Thead, Tr, Th, Tbody, Td, ActionsColumn } from '@patternfly/react-table'
+import React from 'react'
+import { Portfolio, Status } from '../../shapes'
+import TableEmptyState from '../util/TableEmptyState'
+import ScenarioState from './ScenarioState'
+import { useDeleteScenario } from '../../data/project'
+
+function ScenarioTable({ portfolio, status }) {
+ const { mutate: deleteScenario } = useDeleteScenario()
+ const projectId = portfolio?.project?.id
+ const scenarios = portfolio?.scenarios ?? []
+
+ const actions = ({ number }) => [
+ {
+ title: 'Delete Scenario',
+ onClick: () => deleteScenario({ projectId: projectId, number }),
+ isDisabled: number === 0,
+ },
+ ]
+
+ return (
+ <TableComposable aria-label="Scenario List" variant="compact">
+ <Thead>
+ <Tr>
+ <Th>Name</Th>
+ <Th>Topology</Th>
+ <Th>Trace</Th>
+ <Th>State</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {scenarios.map((scenario) => (
+ <Tr key={scenario.id}>
+ <Td dataLabel="Name">{scenario.name}</Td>
+ <Td dataLabel="Topology">
+ {scenario.topology ? (
+ <Link href={`/projects/${projectId}/topologies/${scenario.topology.number}`}>
+ {scenario.topology.name}
+ </Link>
+ ) : (
+ 'Unknown Topology'
+ )}
+ </Td>
+ <Td dataLabel="Workload">{`${scenario.workload.trace.name} (${
+ scenario.workload.samplingFraction * 100
+ }%)`}</Td>
+ <Td dataLabel="State">
+ <ScenarioState state={scenario.jobs[scenario.jobs.length - 1].state} />
+ </Td>
+ <Td isActionCell>
+ <ActionsColumn items={actions(scenario)} />
+ </Td>
+ </Tr>
+ ))}
+ {scenarios.length === 0 && (
+ <Tr>
+ <Td colSpan={4}>
+ <Bullseye>
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading Scenarios"
+ emptyTitle="No scenarios"
+ emptyText="You have not created any scenario for this portfolio yet. Click the New Scenario button to create one."
+ />
+ </Bullseye>
+ </Td>
+ </Tr>
+ )}
+ </Tbody>
+ </TableComposable>
+ )
+}
+
+ScenarioTable.propTypes = {
+ portfolio: Portfolio,
+ status: Status.isRequired,
+}
+
+export default ScenarioTable
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/FilterPanel.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/FilterPanel.js
new file mode 100644
index 00000000..5aaa56ac
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/FilterPanel.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'
+import { filterPanel } from './FilterPanel.module.css'
+
+export const FILTERS = { SHOW_ALL: 'All Projects', SHOW_OWN: 'My Projects', SHOW_SHARED: 'Shared with me' }
+
+const FilterPanel = ({ onSelect, activeFilter = 'SHOW_ALL' }) => (
+ <ToggleGroup className={`${filterPanel} pf-u-mb-sm`}>
+ {Object.keys(FILTERS).map((filter) => (
+ <ToggleGroupItem
+ key={filter}
+ onChange={() => activeFilter === filter || onSelect(filter)}
+ isSelected={activeFilter === filter}
+ text={FILTERS[filter]}
+ />
+ ))}
+ </ToggleGroup>
+)
+
+FilterPanel.propTypes = {
+ onSelect: PropTypes.func.isRequired,
+ activeFilter: PropTypes.string,
+}
+
+export default FilterPanel
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/FilterPanel.module.css b/opendc-web/opendc-web-server/src/main/webui/components/projects/FilterPanel.module.css
new file mode 100644
index 00000000..15c36821
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/FilterPanel.module.css
@@ -0,0 +1,7 @@
+.filterPanel {
+ display: flex;
+}
+
+.filterPanel > button {
+ flex: 1 !important;
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/NewPortfolio.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewPortfolio.js
new file mode 100644
index 00000000..aebcc3c9
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewPortfolio.js
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { PlusIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
+import { useState } from 'react'
+import { useNewPortfolio } from '../../data/project'
+import NewPortfolioModal from './NewPortfolioModal'
+
+function NewPortfolio({ projectId }) {
+ const [isVisible, setVisible] = useState(false)
+ const { mutate: addPortfolio } = useNewPortfolio()
+
+ const onSubmit = (name, targets) => {
+ addPortfolio({ projectId, name, targets })
+ setVisible(false)
+ }
+
+ return (
+ <>
+ <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}>
+ New Portfolio
+ </Button>
+ <NewPortfolioModal isOpen={isVisible} onSubmit={onSubmit} onCancel={() => setVisible(false)} />
+ </>
+ )
+}
+
+NewPortfolio.propTypes = {
+ projectId: PropTypes.number,
+}
+
+export default NewPortfolio
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/NewPortfolioModal.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewPortfolioModal.js
new file mode 100644
index 00000000..ba4bc819
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewPortfolioModal.js
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React, { useRef, useState } from 'react'
+import {
+ Form,
+ FormGroup,
+ FormSection,
+ NumberInput,
+ Select,
+ SelectGroup,
+ SelectOption,
+ SelectVariant,
+ TextInput,
+} from '@patternfly/react-core'
+import Modal from '../util/modals/Modal'
+import { METRIC_GROUPS, METRIC_NAMES } from '../../util/available-metrics'
+
+const NewPortfolioModal = ({ isOpen, onSubmit: onSubmitUpstream, onCancel: onUpstreamCancel }) => {
+ const nameInput = useRef(null)
+ const [repeats, setRepeats] = useState(1)
+ const [isSelectOpen, setSelectOpen] = useState(false)
+ const [selectedMetrics, setSelectedMetrics] = useState([])
+
+ const [isSubmitted, setSubmitted] = useState(false)
+ const [errors, setErrors] = useState({})
+
+ const clearState = () => {
+ setSubmitted(false)
+ setErrors({})
+ nameInput.current.value = ''
+ setRepeats(1)
+ setSelectOpen(false)
+ setSelectedMetrics([])
+ }
+
+ const onSubmit = (event) => {
+ setSubmitted(true)
+
+ if (event) {
+ event.preventDefault()
+ }
+
+ const name = nameInput.current.value
+
+ if (!name) {
+ setErrors({ name: true })
+ return false
+ } else {
+ onSubmitUpstream(name, { metrics: selectedMetrics, repeats })
+ }
+
+ clearState()
+ return false
+ }
+ const onCancel = () => {
+ onUpstreamCancel()
+ clearState()
+ }
+
+ const onSelect = (event, selection) => {
+ if (selectedMetrics.includes(selection)) {
+ setSelectedMetrics((metrics) => metrics.filter((item) => item !== selection))
+ } else {
+ setSelectedMetrics((metrics) => [...metrics, selection])
+ }
+ }
+
+ return (
+ <Modal title="New Portfolio" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form onSubmit={onSubmit}>
+ <FormSection>
+ <FormGroup
+ label="Name"
+ fieldId="name"
+ isRequired
+ validated={isSubmitted && errors.name ? 'error' : 'default'}
+ helperTextInvalid="This field cannot be empty"
+ >
+ <TextInput
+ name="name"
+ id="name"
+ type="text"
+ isRequired
+ ref={nameInput}
+ placeholder="My Portfolio"
+ />
+ </FormGroup>
+ </FormSection>
+ <FormSection title="Targets" titleElement="h4">
+ <FormGroup label="Metrics" fieldId="metrics">
+ <Select
+ variant={SelectVariant.typeaheadMulti}
+ typeAheadAriaLabel="Select a metric"
+ onToggle={() => setSelectOpen(!isSelectOpen)}
+ onSelect={onSelect}
+ onClear={() => setSelectedMetrics([])}
+ selections={selectedMetrics}
+ isOpen={isSelectOpen}
+ placeholderText="Select a metric"
+ menuAppendTo="parent"
+ maxHeight="300px"
+ chipGroupProps={{ numChips: 1 }}
+ isGrouped
+ >
+ {Object.entries(METRIC_GROUPS).map(([group, metrics]) => (
+ <SelectGroup label={group} key={group}>
+ {metrics.map((metric) => (
+ <SelectOption key={metric} value={metric}>
+ {METRIC_NAMES[metric]}
+ </SelectOption>
+ ))}
+ </SelectGroup>
+ ))}
+ </Select>
+ </FormGroup>
+ <FormGroup label="Repeats per Scenario" fieldId="repeats">
+ <NumberInput
+ id="repeats"
+ inputName="repeats"
+ type="number"
+ value={repeats}
+ onChange={(e) => setRepeats(Number(e.target.value))}
+ onPlus={() => setRepeats((r) => r + 1)}
+ onMinus={() => setRepeats((r) => r - 1)}
+ min={1}
+ />
+ </FormGroup>
+ </FormSection>
+ </Form>
+ </Modal>
+ )
+}
+
+NewPortfolioModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+}
+
+export default NewPortfolioModal
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopology.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopology.js
new file mode 100644
index 00000000..4c569c56
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopology.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { PlusIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
+import { useState } from 'react'
+import { useNewTopology } from '../../data/topology'
+import NewTopologyModal from './NewTopologyModal'
+
+function NewTopology({ projectId }) {
+ const [isVisible, setVisible] = useState(false)
+ const { mutate: addTopology } = useNewTopology()
+
+ const onSubmit = (topology) => {
+ addTopology(topology)
+ setVisible(false)
+ }
+ return (
+ <>
+ <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}>
+ New Topology
+ </Button>
+ <NewTopologyModal
+ projectId={projectId}
+ isOpen={isVisible}
+ onSubmit={onSubmit}
+ onCancel={() => setVisible(false)}
+ />
+ </>
+ )
+}
+
+NewTopology.propTypes = {
+ projectId: PropTypes.number,
+}
+
+export default NewTopology
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopologyModal.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopologyModal.js
new file mode 100644
index 00000000..780ec034
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopologyModal.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import produce from 'immer'
+import PropTypes from 'prop-types'
+import React, { useRef, useState } from 'react'
+import { Form, FormGroup, FormSelect, FormSelectOption, TextInput } from '@patternfly/react-core'
+import { useTopologies } from '../../data/topology'
+import Modal from '../util/modals/Modal'
+
+const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) => {
+ const nameInput = useRef(null)
+ const [isSubmitted, setSubmitted] = useState(false)
+ const [originTopology, setOriginTopology] = useState(-1)
+ const [errors, setErrors] = useState({})
+
+ const { data: topologies = [] } = useTopologies(projectId, { enabled: isOpen })
+
+ const clearState = () => {
+ if (nameInput.current) {
+ nameInput.current.value = ''
+ }
+ setSubmitted(false)
+ setOriginTopology(-1)
+ setErrors({})
+ }
+
+ const onSubmit = (event) => {
+ setSubmitted(true)
+
+ if (event) {
+ event.preventDefault()
+ }
+
+ const name = nameInput.current.value
+
+ if (!name) {
+ setErrors({ name: true })
+ return false
+ } else {
+ const candidate = topologies.find((topology) => topology.id === originTopology) || { rooms: [] }
+ const topology = produce(candidate, (draft) => {
+ delete draft.project
+ draft.projectId = projectId
+ draft.name = name
+ })
+ onSubmitUpstream(topology)
+ }
+
+ clearState()
+ return true
+ }
+
+ const onCancel = () => {
+ onCancelUpstream()
+ clearState()
+ }
+
+ return (
+ <Modal title="New Topology" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form onSubmit={onSubmit}>
+ <FormGroup
+ label="Name"
+ fieldId="name"
+ isRequired
+ validated={isSubmitted && errors.name ? 'error' : 'default'}
+ helperTextInvalid="This field cannot be empty"
+ >
+ <TextInput id="name" name="name" type="text" isRequired ref={nameInput} />
+ </FormGroup>
+ <FormGroup label="Topology to duplicate" fieldId="origin" isRequired>
+ <FormSelect
+ id="origin"
+ name="origin"
+ value={originTopology}
+ onChange={(v) => setOriginTopology(+v)}
+ >
+ <FormSelectOption value={-1} key={-1} label="None - start from scratch" />
+ {topologies.map((topology) => (
+ <FormSelectOption value={topology.id} key={topology.id} label={topology.name} />
+ ))}
+ </FormSelect>
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+}
+
+NewTopologyModal.propTypes = {
+ projectId: PropTypes.number,
+ isOpen: PropTypes.bool.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+}
+
+export default NewTopologyModal
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/PortfolioTable.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/PortfolioTable.js
new file mode 100644
index 00000000..0afeaeaf
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/PortfolioTable.js
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Bullseye } from '@patternfly/react-core'
+import PropTypes from 'prop-types'
+import Link from 'next/link'
+import { TableComposable, Thead, Tbody, Tr, Th, Td, ActionsColumn } from '@patternfly/react-table'
+import React from 'react'
+import TableEmptyState from '../util/TableEmptyState'
+import { usePortfolios, useDeletePortfolio } from '../../data/project'
+
+function PortfolioTable({ projectId }) {
+ const { status, data: portfolios = [] } = usePortfolios(projectId)
+ const { mutate: deletePortfolio } = useDeletePortfolio()
+
+ const actions = (portfolio) => [
+ {
+ title: 'Delete Portfolio',
+ onClick: () => deletePortfolio({ projectId, number: portfolio.number }),
+ },
+ ]
+
+ return (
+ <TableComposable aria-label="Portfolio List" variant="compact">
+ <Thead>
+ <Tr>
+ <Th>Name</Th>
+ <Th>Scenarios</Th>
+ <Th>Metrics</Th>
+ <Th>Repeats</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {portfolios.map((portfolio) => (
+ <Tr key={portfolio.id}>
+ <Td dataLabel="Name">
+ <Link href={`/projects/${projectId}/portfolios/${portfolio.number}`}>{portfolio.name}</Link>
+ </Td>
+ <Td dataLabel="Scenarios">
+ {portfolio.scenarios.length === 1
+ ? '1 scenario'
+ : `${portfolio.scenarios.length} scenarios`}
+ </Td>
+ <Td dataLabel="Metrics">
+ {portfolio.targets.metrics.length === 1
+ ? '1 metric'
+ : `${portfolio.targets.metrics.length} metrics`}
+ </Td>
+ <Td dataLabel="Repeats">
+ {portfolio.targets.repeats === 1 ? '1 repeat' : `${portfolio.targets.repeats} repeats`}
+ </Td>
+ <Td isActionCell>
+ <ActionsColumn items={actions(portfolio)} />
+ </Td>
+ </Tr>
+ ))}
+ {portfolios.length === 0 && (
+ <Tr>
+ <Td colSpan={4}>
+ <Bullseye>
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading portfolios"
+ emptyTitle="No portfolios"
+ emptyText="You have not created any portfolio for this project yet. Click the New Portfolio button to create one."
+ />
+ </Bullseye>
+ </Td>
+ </Tr>
+ )}
+ </Tbody>
+ </TableComposable>
+ )
+}
+
+PortfolioTable.propTypes = {
+ projectId: PropTypes.number,
+}
+
+export default PortfolioTable
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectCollection.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectCollection.js
new file mode 100644
index 00000000..a26fed46
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectCollection.js
@@ -0,0 +1,137 @@
+import Link from 'next/link'
+import {
+ Gallery,
+ Bullseye,
+ EmptyState,
+ EmptyStateIcon,
+ Card,
+ CardTitle,
+ CardActions,
+ DropdownItem,
+ CardHeader,
+ Dropdown,
+ KebabToggle,
+ CardBody,
+ CardHeaderMain,
+ TextVariants,
+ Text,
+ TextContent,
+ Tooltip,
+ Button,
+ Label,
+} from '@patternfly/react-core'
+import { PlusIcon, FolderIcon, TrashIcon } from '@patternfly/react-icons'
+import PropTypes from 'prop-types'
+import React, { useReducer, useMemo } from 'react'
+import { Project, Status } from '../../shapes'
+import { parseAndFormatDateTime } from '../../util/date-time'
+import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP, AUTH_NAME_MAP } from '../../util/authorizations'
+import TableEmptyState from '../util/TableEmptyState'
+
+function ProjectCard({ project, onDelete }) {
+ const [isKebabOpen, toggleKebab] = useReducer((t) => !t, false)
+ const { id, role, name, updatedAt } = project
+ const Icon = AUTH_ICON_MAP[role]
+
+ return (
+ <Card
+ isCompact
+ isRounded
+ isFlat
+ className="pf-u-min-height"
+ style={{ '--pf-u-min-height--MinHeight': '175px' }}
+ >
+ <CardHeader className="pf-u-flex-grow-1">
+ <CardHeaderMain className="pf-u-align-self-flex-start">
+ <FolderIcon />
+ </CardHeaderMain>
+ <CardActions>
+ <Tooltip content={AUTH_DESCRIPTION_MAP[role]}>
+ <Label icon={<Icon />}>{AUTH_NAME_MAP[role]}</Label>
+ </Tooltip>
+ <Dropdown
+ isPlain
+ position="right"
+ toggle={<KebabToggle className="pf-u-px-0" onToggle={toggleKebab} />}
+ isOpen={isKebabOpen}
+ dropdownItems={[
+ <DropdownItem
+ key="trash"
+ onClick={() => {
+ onDelete()
+ toggleKebab()
+ }}
+ position="right"
+ icon={<TrashIcon />}
+ >
+ Delete
+ </DropdownItem>,
+ ]}
+ />
+ </CardActions>
+ </CardHeader>
+ <CardTitle component={Link} className="pf-u-pb-0" href={`/projects/${id}`}>
+ {name}
+ </CardTitle>
+ <CardBody isFilled={false}>
+ <TextContent>
+ <Text component={TextVariants.small}>Last modified {parseAndFormatDateTime(updatedAt)}</Text>
+ </TextContent>
+ </CardBody>
+ </Card>
+ )
+}
+
+function ProjectCollection({ status, projects, onDelete, onCreate, isFiltering }) {
+ const sortedProjects = useMemo(() => {
+ const res = [...projects]
+ res.sort((a, b) => (new Date(a.updatedAt) < new Date(b.updatedAt) ? 1 : -1))
+ return res
+ }, [projects])
+
+ if (sortedProjects.length === 0) {
+ return (
+ <TableEmptyState
+ status={status}
+ isFiltering={isFiltering}
+ loadingTitle="Loading Projects"
+ emptyTitle="No projects"
+ emptyText="You have not created any projects yet. Create a new project to get started quickly."
+ emptyAction={
+ <Button icon={<PlusIcon />} onClick={onCreate}>
+ Create Project
+ </Button>
+ }
+ />
+ )
+ }
+
+ return (
+ <Gallery hasGutter aria-label="Available projects">
+ {sortedProjects.map((project) => (
+ <ProjectCard key={project.id} project={project} onDelete={() => onDelete(project)} />
+ ))}
+ <Card isCompact isFlat isRounded style={{ borderStyle: 'dotted' }}>
+ <Bullseye>
+ <EmptyState>
+ <Button isBlock variant="link" onClick={onCreate}>
+ <EmptyStateIcon icon={PlusIcon} />
+ <br />
+ Create Project
+ </Button>
+ </EmptyState>
+ </Bullseye>
+ </Card>
+ </Gallery>
+ )
+}
+
+ProjectCollection.propTypes = {
+ status: Status.isRequired,
+ isFiltering: PropTypes.bool,
+ projects: PropTypes.arrayOf(Project).isRequired,
+ onDelete: PropTypes.func,
+ onCreate: PropTypes.func,
+}
+
+export default ProjectCollection
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectOverview.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectOverview.js
new file mode 100644
index 00000000..3e1656f6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectOverview.js
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import {
+ Card,
+ CardActions,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Grid,
+ GridItem,
+ Skeleton,
+} from '@patternfly/react-core'
+import NewTopology from './NewTopology'
+import TopologyTable from './TopologyTable'
+import NewPortfolio from './NewPortfolio'
+import PortfolioTable from './PortfolioTable'
+import { useProject } from '../../data/project'
+
+function ProjectOverview({ projectId }) {
+ const { data: project } = useProject(projectId)
+
+ return (
+ <Grid hasGutter>
+ <GridItem md={2}>
+ <Card>
+ <CardTitle>Details</CardTitle>
+ <CardBody>
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Name</DescriptionListTerm>
+ <DescriptionListDescription>
+ {project?.name ?? <Skeleton screenreaderText="Loading project" />}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={5}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <NewTopology projectId={projectId} />
+ </CardActions>
+ <CardTitle>Topologies</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <TopologyTable projectId={projectId} />
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={5}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <NewPortfolio projectId={projectId} />
+ </CardActions>
+ <CardTitle>Portfolios</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <PortfolioTable projectId={projectId} />
+ </CardBody>
+ </Card>
+ </GridItem>
+ </Grid>
+ )
+}
+
+ProjectOverview.propTypes = {
+ projectId: PropTypes.number,
+}
+
+export default ProjectOverview
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/TopologyTable.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/TopologyTable.js
new file mode 100644
index 00000000..1c2c4f04
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/TopologyTable.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Bullseye, AlertGroup, Alert, AlertVariant, AlertActionCloseButton } from '@patternfly/react-core'
+import PropTypes from 'prop-types'
+import Link from 'next/link'
+import { Tr, Th, Thead, Td, ActionsColumn, Tbody, TableComposable } from '@patternfly/react-table'
+import React, { useState } from 'react'
+import TableEmptyState from '../util/TableEmptyState'
+import { parseAndFormatDateTime } from '../../util/date-time'
+import { useTopologies, useDeleteTopology } from '../../data/topology'
+
+function TopologyTable({ projectId }) {
+ const [error, setError] = useState('')
+
+ const { status, data: topologies = [] } = useTopologies(projectId)
+ const { mutate: deleteTopology } = useDeleteTopology({
+ onError: (error) => setError(error),
+ })
+
+ const actions = ({ number }) => [
+ {
+ title: 'Delete Topology',
+ onClick: () => deleteTopology({ projectId, number }),
+ isDisabled: number === 0,
+ },
+ ]
+
+ return (
+ <>
+ <AlertGroup isToast>
+ {error && (
+ <Alert
+ isLiveRegion
+ variant={AlertVariant.danger}
+ title={error}
+ actionClose={
+ <AlertActionCloseButton
+ title={error}
+ variantLabel="danger alert"
+ onClose={() => setError(null)}
+ />
+ }
+ />
+ )}
+ </AlertGroup>
+ <TableComposable aria-label="Topology List" variant="compact">
+ <Thead>
+ <Tr>
+ <Th>Name</Th>
+ <Th>Rooms</Th>
+ <Th>Last Edited</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {topologies.map((topology) => (
+ <Tr key={topology.id}>
+ <Td dataLabel="Name">
+ <Link href={`/projects/${projectId}/topologies/${topology.number}`}>
+ {topology.name}
+ </Link>
+ </Td>
+ <Td dataLabel="Rooms">
+ {topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`}
+ </Td>
+ <Td dataLabel="Last Edited">{parseAndFormatDateTime(topology.updatedAt)}</Td>
+ <Td isActionCell>
+ <ActionsColumn items={actions(topology)} />
+ </Td>
+ </Tr>
+ ))}
+ {topologies.length === 0 && (
+ <Tr>
+ <Td colSpan={3}>
+ <Bullseye>
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading topologies"
+ emptyTitle="No topologies"
+ emptyText="You have not created any topology for this project yet. Click the New Topology button to create one."
+ />
+ </Bullseye>
+ </Td>
+ </Tr>
+ )}
+ </Tbody>
+ </TableComposable>
+ </>
+ )
+}
+
+TopologyTable.propTypes = {
+ projectId: PropTypes.number,
+}
+
+export default TopologyTable
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/RoomTable.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/RoomTable.js
new file mode 100644
index 00000000..7f7b4171
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/RoomTable.js
@@ -0,0 +1,74 @@
+import { Button, Bullseye } from '@patternfly/react-core'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch } from 'react-redux'
+import { useTopology } from '../../data/topology'
+import { Tr, Th, Thead, TableComposable, Td, ActionsColumn, Tbody } from '@patternfly/react-table'
+import { deleteRoom } from '../../redux/actions/topology/room'
+import TableEmptyState from '../util/TableEmptyState'
+
+function RoomTable({ projectId, topologyId, onSelect }) {
+ const dispatch = useDispatch()
+ const { status, data: topology } = useTopology(projectId, topologyId)
+ const onDelete = (room) => dispatch(deleteRoom(room.id))
+ const actions = (room) => [
+ {
+ title: 'Delete room',
+ onClick: () => onDelete(room),
+ },
+ ]
+
+ return (
+ <TableComposable aria-label="Room list" variant="compact">
+ <Thead>
+ <Tr>
+ <Th>Name</Th>
+ <Th>Tiles</Th>
+ <Th>Racks</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {topology?.rooms.map((room) => {
+ const tileCount = room.tiles.length
+ const rackCount = room.tiles.filter((tile) => tile.rack).length
+ return (
+ <Tr key={room.id}>
+ <Td dataLabel="Name">
+ <Button variant="link" isInline onClick={() => onSelect(room)}>
+ {room.name}
+ </Button>
+ </Td>
+ <Td dataLabel="Tiles">{tileCount === 1 ? '1 tile' : `${tileCount} tiles`}</Td>
+ <Td dataLabel="Racks">{rackCount === 1 ? '1 rack' : `${rackCount} racks`}</Td>
+ <Td isActionCell>
+ <ActionsColumn items={actions(room)} />
+ </Td>
+ </Tr>
+ )
+ })}
+ {topology?.rooms.length === 0 && (
+ <Tr>
+ <Td colSpan={4}>
+ <Bullseye>
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading Rooms"
+ emptyTitle="No rooms"
+ emptyText="There are currently no rooms in this topology. Open the Floor Plan to create a room"
+ />
+ </Bullseye>
+ </Td>
+ </Tr>
+ )}
+ </Tbody>
+ </TableComposable>
+ )
+}
+
+RoomTable.propTypes = {
+ projectId: PropTypes.number,
+ topologyId: PropTypes.number,
+ onSelect: PropTypes.func,
+}
+
+export default RoomTable
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/TopologyMap.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/TopologyMap.js
new file mode 100644
index 00000000..ff583750
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/TopologyMap.js
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { useState, useRef } from 'react'
+import {
+ Bullseye,
+ Drawer,
+ DrawerContent,
+ DrawerContentBody,
+ EmptyState,
+ EmptyStateIcon,
+ Spinner,
+ Title,
+} from '@patternfly/react-core'
+import MapStage from './map/MapStage'
+import Collapse from './map/controls/Collapse'
+import { useSelector } from 'react-redux'
+import TopologySidebar from './sidebar/TopologySidebar'
+
+function TopologyMap() {
+ const topologyIsLoading = useSelector((state) => !state.topology.root)
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+
+ const [isExpanded, setExpanded] = useState(true)
+ const panelContent = <TopologySidebar interactionLevel={interactionLevel} onClose={() => setExpanded(false)} />
+
+ const hotkeysRef = useRef()
+
+ return topologyIsLoading ? (
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={Spinner} />
+ <Title size="lg" headingLevel="h4">
+ Loading Topology
+ </Title>
+ </EmptyState>
+ </Bullseye>
+ ) : (
+ <Drawer isExpanded={isExpanded}>
+ <DrawerContent panelContent={panelContent}>
+ <DrawerContentBody style={{ position: 'relative' }}>
+ <MapStage hotkeysRef={hotkeysRef} />
+ <Collapse onClick={() => setExpanded(true)} />
+ </DrawerContentBody>
+ </DrawerContent>
+ </Drawer>
+ )
+}
+
+export default TopologyMap
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/TopologyOverview.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/TopologyOverview.js
new file mode 100644
index 00000000..f8ee4990
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/TopologyOverview.js
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import {
+ Card,
+ CardBody,
+ CardTitle,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Grid,
+ GridItem,
+ Skeleton,
+} from '@patternfly/react-core'
+import React from 'react'
+import { useTopology } from '../../data/topology'
+import { parseAndFormatDateTime } from '../../util/date-time'
+import RoomTable from './RoomTable'
+
+function TopologyOverview({ projectId, topologyNumber, onSelect }) {
+ const { data: topology } = useTopology(projectId, topologyNumber)
+ return (
+ <Grid hasGutter>
+ <GridItem md={2}>
+ <Card>
+ <CardTitle>Details</CardTitle>
+ <CardBody>
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Name</DescriptionListTerm>
+ <DescriptionListDescription>
+ {topology?.name ?? <Skeleton screenreaderText="Loading topology" />}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Last edited</DescriptionListTerm>
+ <DescriptionListDescription>
+ {topology ? (
+ parseAndFormatDateTime(topology.updatedAt)
+ ) : (
+ <Skeleton screenreaderText="Loading topology" />
+ )}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={5}>
+ <Card>
+ <CardTitle>Rooms</CardTitle>
+ <CardBody>
+ <RoomTable
+ projectId={projectId}
+ topologyId={topologyNumber}
+ onSelect={(room) => onSelect('room', room)}
+ />
+ </CardBody>
+ </Card>
+ </GridItem>
+ </Grid>
+ )
+}
+
+TopologyOverview.propTypes = {
+ projectId: PropTypes.number,
+ topologyNumber: PropTypes.number,
+ onSelect: PropTypes.func,
+}
+
+export default TopologyOverview
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/GrayContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/GrayContainer.js
new file mode 100644
index 00000000..ccf637e5
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/GrayContainer.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch } from 'react-redux'
+import { goDownOneInteractionLevel } from '../../../redux/actions/interaction-level'
+import GrayLayer from './elements/GrayLayer'
+
+function GrayContainer() {
+ const dispatch = useDispatch()
+ const onClick = () => dispatch(goDownOneInteractionLevel())
+ return <GrayLayer onClick={onClick} />
+}
+
+export default GrayContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapConstants.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapConstants.js
new file mode 100644
index 00000000..4c3b2757
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapConstants.js
@@ -0,0 +1,25 @@
+export const MAP_SIZE = 50
+export const TILE_SIZE_IN_PIXELS = 100
+export const TILE_SIZE_IN_METERS = 0.5
+export const MAP_SIZE_IN_PIXELS = MAP_SIZE * TILE_SIZE_IN_PIXELS
+
+export const OBJECT_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 5
+export const TILE_PLUS_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 3
+export const OBJECT_SIZE_IN_PIXELS = TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2
+
+export const GRID_LINE_WIDTH_IN_PIXELS = 2
+export const WALL_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16
+export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16
+export const TILE_PLUS_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 10
+
+export const RACK_FILL_ICON_WIDTH = OBJECT_SIZE_IN_PIXELS / 3
+export const RACK_FILL_ICON_OPACITY = 0.8
+
+export const MAP_MOVE_PIXELS_PER_EVENT = 20
+export const MAP_SCALE_PER_EVENT = 1.1
+export const MAP_MIN_SCALE = 0.5
+export const MAP_MAX_SCALE = 1.5
+
+export const MAX_NUM_UNITS_PER_MACHINE = 6
+export const DEFAULT_RACK_SLOT_CAPACITY = 42
+export const DEFAULT_RACK_POWER_CAPACITY = 10000
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapStage.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapStage.js
new file mode 100644
index 00000000..e2b626ec
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapStage.js
@@ -0,0 +1,83 @@
+import React, { useRef, useState } from 'react'
+import PropTypes from 'prop-types'
+import { useHotkeys } from 'react-hotkeys-hook'
+import { Stage } from 'react-konva'
+import { MAP_MAX_SCALE, MAP_MIN_SCALE, MAP_MOVE_PIXELS_PER_EVENT, MAP_SCALE_PER_EVENT } from './MapConstants'
+import useResizeObserver from 'use-resize-observer'
+import { mapContainer } from './MapStage.module.css'
+import MapLayer from './layers/MapLayer'
+import RoomHoverLayer from './layers/RoomHoverLayer'
+import ObjectHoverLayer from './layers/ObjectHoverLayer'
+import ScaleIndicator from './controls/ScaleIndicator'
+import Toolbar from './controls/Toolbar'
+
+function MapStage({ hotkeysRef }) {
+ const stageRef = useRef(null)
+ const { width = 500, height = 500 } = useResizeObserver({ ref: stageRef.current?.attrs?.container })
+ const [[x, y], setPos] = useState([0, 0])
+ const [scale, setScale] = useState(1)
+
+ const clampScale = (target) => Math.min(Math.max(target, MAP_MIN_SCALE), MAP_MAX_SCALE)
+ const moveWithDelta = (deltaX, deltaY) => setPos(([x, y]) => [x + deltaX, y + deltaY])
+
+ const onZoom = (e) => {
+ e.evt.preventDefault()
+
+ const stage = stageRef.current.getStage()
+ const oldScale = scale
+
+ const pointer = stage.getPointerPosition()
+ const mousePointTo = {
+ x: (pointer.x - x) / oldScale,
+ y: (pointer.y - y) / oldScale,
+ }
+
+ const newScale = clampScale(e.evt.deltaY > 0 ? oldScale * MAP_SCALE_PER_EVENT : oldScale / MAP_SCALE_PER_EVENT)
+
+ setScale(newScale)
+ setPos([pointer.x - mousePointTo.x * newScale, pointer.y - mousePointTo.y * newScale])
+ }
+ const onZoomButton = (zoomIn) =>
+ setScale((scale) => clampScale(zoomIn ? scale * MAP_SCALE_PER_EVENT : scale / MAP_SCALE_PER_EVENT))
+ const onDragEnd = (e) => setPos([e.target.x(), e.target.y()])
+ const onExport = () => {
+ const download = document.createElement('a')
+ download.href = stageRef.current.getStage().toDataURL()
+ download.download = 'opendc-canvas-export-' + Date.now() + '.png'
+ download.click()
+ }
+
+ useHotkeys('left, a', () => moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0), { element: hotkeysRef.current })
+ useHotkeys('right, d', () => moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0), { element: hotkeysRef.current })
+ useHotkeys('up, w', () => moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT), { element: hotkeysRef.current })
+ useHotkeys('down, s', () => moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT), { element: hotkeysRef.current })
+
+ return (
+ <>
+ <Stage
+ className={mapContainer}
+ ref={stageRef}
+ onWheel={onZoom}
+ onDragEnd={onDragEnd}
+ draggable
+ width={width}
+ height={height}
+ scale={{ x: scale, y: scale }}
+ x={x}
+ y={y}
+ >
+ <MapLayer />
+ <RoomHoverLayer />
+ <ObjectHoverLayer />
+ </Stage>
+ <ScaleIndicator scale={scale} />
+ <Toolbar onZoom={onZoomButton} onExport={onExport} />
+ </>
+ )
+}
+
+MapStage.propTypes = {
+ hotkeysRef: PropTypes.object.isRequired,
+}
+
+export default MapStage
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapStage.module.css b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapStage.module.css
new file mode 100644
index 00000000..47c3dde2
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/MapStage.module.css
@@ -0,0 +1,29 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.mapContainer {
+ background-color: var(--pf-global--Color--light-200);
+ position: relative;
+ display: flex;
+ width: 100%;
+ height: 100%;
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackContainer.js
new file mode 100644
index 00000000..14449a91
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackContainer.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useSelector } from 'react-redux'
+import { Tile } from '../../../shapes'
+import RackGroup from './groups/RackGroup'
+
+function RackContainer({ tile }) {
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+ return <RackGroup interactionLevel={interactionLevel} tile={tile} />
+}
+
+RackContainer.propTypes = {
+ tile: Tile,
+}
+
+export default RackContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackEnergyFillContainer.js
new file mode 100644
index 00000000..a1ca7426
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackEnergyFillContainer.js
@@ -0,0 +1,36 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { useSelector } from 'react-redux'
+import RackFillBar from './elements/RackFillBar'
+
+function RackSpaceFillContainer({ rackId, ...props }) {
+ const fillFraction = useSelector((state) => {
+ const rack = state.topology.racks[rackId]
+ if (!rack) {
+ return 0
+ }
+
+ const { machines, cpus, gpus, memories, storages } = state.topology
+ let energyConsumptionTotal = 0
+
+ for (const machineId of rack.machines) {
+ if (!machineId) {
+ continue
+ }
+ const machine = machines[machineId]
+ machine.cpus.forEach((id) => (energyConsumptionTotal += cpus[id].energyConsumptionW))
+ machine.gpus.forEach((id) => (energyConsumptionTotal += gpus[id].energyConsumptionW))
+ machine.memories.forEach((id) => (energyConsumptionTotal += memories[id].energyConsumptionW))
+ machine.storages.forEach((id) => (energyConsumptionTotal += storages[id].energyConsumptionW))
+ }
+
+ return Math.min(1, energyConsumptionTotal / rack.powerCapacityW)
+ })
+ return <RackFillBar {...props} type="energy" fillFraction={fillFraction} />
+}
+
+RackSpaceFillContainer.propTypes = {
+ rackId: PropTypes.string.isRequired,
+}
+
+export default RackSpaceFillContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackSpaceFillContainer.js
new file mode 100644
index 00000000..2039a9d3
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RackSpaceFillContainer.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+import { useSelector } from 'react-redux'
+import RackFillBar from './elements/RackFillBar'
+
+function RackSpaceFillContainer({ rackId, ...props }) {
+ const rack = useSelector((state) => state.topology.racks[rackId])
+
+ if (!rack) {
+ return null
+ }
+
+ return <RackFillBar {...props} type="space" fillFraction={rack.machines.length / rack.capacity} />
+}
+
+RackSpaceFillContainer.propTypes = {
+ rackId: PropTypes.string.isRequired,
+}
+
+export default RackSpaceFillContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RoomContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RoomContainer.js
new file mode 100644
index 00000000..76785bea
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/RoomContainer.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { goFromBuildingToRoom } from '../../../redux/actions/interaction-level'
+import RoomGroup from './groups/RoomGroup'
+
+function RoomContainer({ roomId, ...props }) {
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+ const currentRoomInConstruction = useSelector((state) => state.construction.currentRoomInConstruction)
+ const room = useSelector((state) => state.topology.rooms[roomId])
+ const dispatch = useDispatch()
+
+ if (!room) {
+ return null
+ }
+
+ return (
+ <RoomGroup
+ {...props}
+ interactionLevel={interactionLevel}
+ currentRoomInConstruction={currentRoomInConstruction}
+ room={room}
+ onClick={() => dispatch(goFromBuildingToRoom(roomId))}
+ />
+ )
+}
+
+RoomContainer.propTypes = {
+ roomId: PropTypes.string,
+}
+
+export default RoomContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/TileContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/TileContainer.js
new file mode 100644
index 00000000..0788b894
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/TileContainer.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+import { useDispatch, useSelector } from 'react-redux'
+import { goFromRoomToRack } from '../../../redux/actions/interaction-level'
+import TileGroup from './groups/TileGroup'
+
+function TileContainer({ tileId, ...props }) {
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+ const dispatch = useDispatch()
+ const tile = useSelector((state) => state.topology.tiles[tileId])
+
+ if (!tile) {
+ return null
+ }
+
+ const onClick = (tile) => {
+ if (tile.rack) {
+ dispatch(goFromRoomToRack(tile.id))
+ }
+ }
+ return <TileGroup {...props} onClick={onClick} tile={tile} interactionLevel={interactionLevel} />
+}
+
+TileContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default TileContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/TopologyContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/TopologyContainer.js
new file mode 100644
index 00000000..cc0d46b3
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/TopologyContainer.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useSelector } from 'react-redux'
+import TopologyGroup from './groups/TopologyGroup'
+
+function TopologyContainer() {
+ const topology = useSelector((state) => state.topology.root)
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+
+ return <TopologyGroup topology={topology} interactionLevel={interactionLevel} />
+}
+
+export default TopologyContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/WallContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/WallContainer.js
new file mode 100644
index 00000000..106d8d3d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/WallContainer.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+import { useSelector } from 'react-redux'
+import WallGroup from './groups/WallGroup'
+
+function WallContainer({ roomId, ...props }) {
+ const tiles = useSelector((state) => {
+ return state.topology.rooms[roomId]?.tiles.map((tileId) => state.topology.tiles[tileId]) ?? []
+ })
+ return <WallGroup {...props} tiles={tiles} />
+}
+
+WallContainer.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default WallContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Collapse.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Collapse.js
new file mode 100644
index 00000000..931ded94
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Collapse.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { ChevronLeftIcon } from '@patternfly/react-icons'
+import { collapseContainer } from './Collapse.module.css'
+import { Button } from '@patternfly/react-core'
+
+function Collapse({ onClick }) {
+ return (
+ <div className={collapseContainer}>
+ <Button variant="tertiary" onClick={onClick}>
+ <ChevronLeftIcon />
+ </Button>
+ </div>
+ )
+}
+
+Collapse.propTypes = {
+ onClick: PropTypes.func,
+}
+
+export default Collapse
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Collapse.module.css b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Collapse.module.css
new file mode 100644
index 00000000..70fd465f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Collapse.module.css
@@ -0,0 +1,55 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.collapseContainer {
+ position: absolute;
+ right: var(--pf-global--spacer--xs);
+ top: 0;
+ bottom: 10%;
+ margin: auto 0;
+ height: 50px;
+}
+
+.collapseContainer > button:global(.pf-m-tertiary) {
+ height: 100%;
+ padding: 2px;
+
+ margin-right: var(--pf-global--spacer--xs);
+ margin-top: var(--pf-global--spacer--xs);
+ background-color: var(--pf-global--BackgroundColor--100);
+ border: none;
+ border-radius: var(--pf-global--BorderRadius--sm);
+ box-shadow: var(--pf-global--BoxShadow--sm);
+}
+
+.collapseContainer > button:global(.pf-m-tertiary):not(:global(.pf-m-disabled)) {
+ background-color: var(--pf-global--BackgroundColor--100);
+}
+
+.collapseContainer > button:global(.pf-m-tertiary):after {
+ display: none;
+}
+
+.collapseContainer > button:global(.pf-m-tertiary):hover {
+ border: none;
+ box-shadow: var(--pf-global--BoxShadow--md);
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/ScaleIndicator.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/ScaleIndicator.js
new file mode 100644
index 00000000..3ec893fb
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/ScaleIndicator.js
@@ -0,0 +1,18 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { TILE_SIZE_IN_METERS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
+import { scaleIndicator } from './ScaleIndicator.module.css'
+
+function ScaleIndicator({ scale }) {
+ return (
+ <div className={scaleIndicator} style={{ width: TILE_SIZE_IN_PIXELS * scale }}>
+ {TILE_SIZE_IN_METERS}m
+ </div>
+ )
+}
+
+ScaleIndicator.propTypes = {
+ scale: PropTypes.number.isRequired,
+}
+
+export default ScaleIndicator
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/ScaleIndicator.module.css b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/ScaleIndicator.module.css
new file mode 100644
index 00000000..f19e0ff2
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/ScaleIndicator.module.css
@@ -0,0 +1,10 @@
+.scaleIndicator {
+ position: absolute;
+ right: 10px;
+ bottom: 10px;
+ z-index: 50;
+
+ border: solid 2px #212529;
+ border-top: none;
+ border-left: none;
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Toolbar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Toolbar.js
new file mode 100644
index 00000000..00aaf3e1
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Toolbar.js
@@ -0,0 +1,33 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { control, toolBar } from './Toolbar.module.css'
+import { Button } from '@patternfly/react-core'
+import { SearchPlusIcon, SearchMinusIcon, CameraIcon } from '@patternfly/react-icons'
+
+function Toolbar({ onZoom, onExport }) {
+ return (
+ <div className={toolBar}>
+ <Button variant="tertiary" title="Zoom in" onClick={() => onZoom(true)} className={control}>
+ <SearchPlusIcon />
+ </Button>
+ <Button variant="tertiary" title="Zoom out" onClick={() => onZoom(false)} className={control}>
+ <SearchMinusIcon />
+ </Button>
+ <Button
+ variant="tertiary"
+ title="Export Canvas to PNG Image"
+ onClick={() => onExport()}
+ className={control}
+ >
+ <CameraIcon />
+ </Button>
+ </div>
+ )
+}
+
+Toolbar.propTypes = {
+ onZoom: PropTypes.func,
+ onExport: PropTypes.func,
+}
+
+export default Toolbar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Toolbar.module.css b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Toolbar.module.css
new file mode 100644
index 00000000..007389da
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/controls/Toolbar.module.css
@@ -0,0 +1,27 @@
+.toolBar {
+ position: absolute;
+ bottom: var(--pf-global--spacer--md);
+ left: var(--pf-global--spacer--xl);
+}
+
+.control:global(.pf-m-tertiary) {
+ margin-right: var(--pf-global--spacer--xs);
+ margin-top: var(--pf-global--spacer--xs);
+ background-color: var(--pf-global--BackgroundColor--100);
+ border: none;
+ border-radius: var(--pf-global--BorderRadius--sm);
+ box-shadow: var(--pf-global--BoxShadow--sm);
+}
+
+.control:global(.pf-m-tertiary):not(:global(.pf-m-disabled)) {
+ background-color: var(--pf-global--BackgroundColor--100);
+}
+
+.control:global(.pf-m-tertiary):after {
+ display: none;
+}
+
+.control:global(.pf-m-tertiary):hover {
+ border: none;
+ box-shadow: var(--pf-global--BoxShadow--md);
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/Backdrop.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/Backdrop.js
new file mode 100644
index 00000000..93037b51
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/Backdrop.js
@@ -0,0 +1,10 @@
+import React from 'react'
+import { Rect } from 'react-konva'
+import { BACKDROP_COLOR } from '../../../../util/colors'
+import { MAP_SIZE_IN_PIXELS } from '../MapConstants'
+
+function Backdrop() {
+ return <Rect x={0} y={0} width={MAP_SIZE_IN_PIXELS} height={MAP_SIZE_IN_PIXELS} fill={BACKDROP_COLOR} />
+}
+
+export default Backdrop
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/GrayLayer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/GrayLayer.js
new file mode 100644
index 00000000..08c687f6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/GrayLayer.js
@@ -0,0 +1,24 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Rect } from 'react-konva'
+import { GRAYED_OUT_AREA_COLOR } from '../../../../util/colors'
+import { MAP_SIZE_IN_PIXELS } from '../MapConstants'
+
+function GrayLayer({ onClick }) {
+ return (
+ <Rect
+ x={0}
+ y={0}
+ width={MAP_SIZE_IN_PIXELS}
+ height={MAP_SIZE_IN_PIXELS}
+ fill={GRAYED_OUT_AREA_COLOR}
+ onClick={onClick}
+ />
+ )
+}
+
+GrayLayer.propTypes = {
+ onClick: PropTypes.func,
+}
+
+export default GrayLayer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/HoverTile.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/HoverTile.js
new file mode 100644
index 00000000..20c2c6d1
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/HoverTile.js
@@ -0,0 +1,30 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Rect } from 'react-konva'
+import { ROOM_HOVER_INVALID_COLOR, ROOM_HOVER_VALID_COLOR } from '../../../../util/colors'
+import { TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+function HoverTile({ x, y, isValid, scale = 1, onClick }) {
+ return (
+ <Rect
+ x={x}
+ y={y}
+ scaleX={scale}
+ scaleY={scale}
+ width={TILE_SIZE_IN_PIXELS}
+ height={TILE_SIZE_IN_PIXELS}
+ fill={isValid ? ROOM_HOVER_VALID_COLOR : ROOM_HOVER_INVALID_COLOR}
+ onClick={onClick}
+ />
+ )
+}
+
+HoverTile.propTypes = {
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired,
+ isValid: PropTypes.bool.isRequired,
+ scale: PropTypes.number,
+ onClick: PropTypes.func.isRequired,
+}
+
+export default HoverTile
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/ImageComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/ImageComponent.js
new file mode 100644
index 00000000..fdae53f2
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/ImageComponent.js
@@ -0,0 +1,37 @@
+import PropTypes from 'prop-types'
+import React, { useEffect, useState } from 'react'
+import { Image } from 'react-konva'
+
+const imageCaches = {}
+
+function ImageComponent({ src, x, y, width, height, opacity }) {
+ const [image, setImage] = useState(null)
+
+ useEffect(() => {
+ if (imageCaches[src]) {
+ setImage(imageCaches[src])
+ return
+ }
+
+ const image = new window.Image()
+ image.src = src
+ image.onload = () => {
+ setImage(image)
+ imageCaches[src] = image
+ }
+ }, [src])
+
+ // eslint-disable-next-line jsx-a11y/alt-text
+ return <Image image={image} x={x} y={y} width={width} height={height} opacity={opacity} />
+}
+
+ImageComponent.propTypes = {
+ src: PropTypes.string.isRequired,
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired,
+ opacity: PropTypes.number.isRequired,
+}
+
+export default ImageComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/RackFillBar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/RackFillBar.js
new file mode 100644
index 00000000..aa284944
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/RackFillBar.js
@@ -0,0 +1,68 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Group, Rect } from 'react-konva'
+import {
+ RACK_ENERGY_BAR_BACKGROUND_COLOR,
+ RACK_ENERGY_BAR_FILL_COLOR,
+ RACK_SPACE_BAR_BACKGROUND_COLOR,
+ RACK_SPACE_BAR_FILL_COLOR,
+} from '../../../../util/colors'
+import {
+ OBJECT_BORDER_WIDTH_IN_PIXELS,
+ OBJECT_MARGIN_IN_PIXELS,
+ RACK_FILL_ICON_OPACITY,
+ RACK_FILL_ICON_WIDTH,
+ TILE_SIZE_IN_PIXELS,
+} from '../MapConstants'
+import ImageComponent from './ImageComponent'
+
+function RackFillBar({ positionX, positionY, type, fillFraction }) {
+ const halfOfObjectBorderWidth = OBJECT_BORDER_WIDTH_IN_PIXELS / 2
+ const x =
+ positionX * TILE_SIZE_IN_PIXELS +
+ OBJECT_MARGIN_IN_PIXELS +
+ (type === 'space' ? halfOfObjectBorderWidth : 0.5 * (TILE_SIZE_IN_PIXELS - 2 * OBJECT_MARGIN_IN_PIXELS))
+ const startY = positionY * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS + halfOfObjectBorderWidth
+ const width = 0.5 * (TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2) - halfOfObjectBorderWidth
+ const fullHeight = TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2 - OBJECT_BORDER_WIDTH_IN_PIXELS
+
+ const fractionHeight = fillFraction * fullHeight
+ const fractionY =
+ (positionY + 1) * TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS - halfOfObjectBorderWidth - fractionHeight
+
+ return (
+ <Group>
+ <Rect
+ x={x}
+ y={startY}
+ width={width}
+ height={fullHeight}
+ fill={type === 'space' ? RACK_SPACE_BAR_BACKGROUND_COLOR : RACK_ENERGY_BAR_BACKGROUND_COLOR}
+ />
+ <Rect
+ x={x}
+ y={fractionY}
+ width={width}
+ height={fractionHeight}
+ fill={type === 'space' ? RACK_SPACE_BAR_FILL_COLOR : RACK_ENERGY_BAR_FILL_COLOR}
+ />
+ <ImageComponent
+ src={'/img/topology/rack-' + type + '-icon.png'}
+ x={x + width * 0.5 - RACK_FILL_ICON_WIDTH * 0.5}
+ y={startY + fullHeight * 0.5 - RACK_FILL_ICON_WIDTH * 0.5}
+ width={RACK_FILL_ICON_WIDTH}
+ height={RACK_FILL_ICON_WIDTH}
+ opacity={RACK_FILL_ICON_OPACITY}
+ />
+ </Group>
+ )
+}
+
+RackFillBar.propTypes = {
+ positionX: PropTypes.number.isRequired,
+ positionY: PropTypes.number.isRequired,
+ type: PropTypes.string.isRequired,
+ fillFraction: PropTypes.number.isRequired,
+}
+
+export default RackFillBar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/RoomTile.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/RoomTile.js
new file mode 100644
index 00000000..e7329dc0
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/RoomTile.js
@@ -0,0 +1,24 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Rect } from 'react-konva'
+import { Tile } from '../../../../shapes'
+import { TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+function RoomTile({ tile, color }) {
+ return (
+ <Rect
+ x={tile.positionX * TILE_SIZE_IN_PIXELS}
+ y={tile.positionY * TILE_SIZE_IN_PIXELS}
+ width={TILE_SIZE_IN_PIXELS}
+ height={TILE_SIZE_IN_PIXELS}
+ fill={color}
+ />
+ )
+}
+
+RoomTile.propTypes = {
+ tile: Tile,
+ color: PropTypes.string,
+}
+
+export default RoomTile
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/TileObject.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/TileObject.js
new file mode 100644
index 00000000..3211f187
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/TileObject.js
@@ -0,0 +1,27 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Rect } from 'react-konva'
+import { OBJECT_BORDER_COLOR } from '../../../../util/colors'
+import { OBJECT_BORDER_WIDTH_IN_PIXELS, OBJECT_MARGIN_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+function TileObject({ positionX, positionY, color }) {
+ return (
+ <Rect
+ x={positionX * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS}
+ y={positionY * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS}
+ width={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2}
+ height={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2}
+ fill={color}
+ stroke={OBJECT_BORDER_COLOR}
+ strokeWidth={OBJECT_BORDER_WIDTH_IN_PIXELS}
+ />
+ )
+}
+
+TileObject.propTypes = {
+ positionX: PropTypes.number.isRequired,
+ positionY: PropTypes.number.isRequired,
+ color: PropTypes.string.isRequired,
+}
+
+export default TileObject
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/TilePlusIcon.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/TilePlusIcon.js
new file mode 100644
index 00000000..186c2b3a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/TilePlusIcon.js
@@ -0,0 +1,44 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Group, Line } from 'react-konva'
+import { TILE_PLUS_COLOR } from '../../../../util/colors'
+import { TILE_PLUS_MARGIN_IN_PIXELS, TILE_PLUS_WIDTH_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+function TilePlusIcon({ x, y, scale = 1 }) {
+ const linePoints = [
+ [
+ x + 0.5 * TILE_SIZE_IN_PIXELS * scale,
+ y + TILE_PLUS_MARGIN_IN_PIXELS * scale,
+ x + 0.5 * TILE_SIZE_IN_PIXELS * scale,
+ y + TILE_SIZE_IN_PIXELS * scale - TILE_PLUS_MARGIN_IN_PIXELS * scale,
+ ],
+ [
+ x + TILE_PLUS_MARGIN_IN_PIXELS * scale,
+ y + 0.5 * TILE_SIZE_IN_PIXELS * scale,
+ x + TILE_SIZE_IN_PIXELS * scale - TILE_PLUS_MARGIN_IN_PIXELS * scale,
+ y + 0.5 * TILE_SIZE_IN_PIXELS * scale,
+ ],
+ ]
+ return (
+ <Group>
+ {linePoints.map((points, index) => (
+ <Line
+ key={index}
+ points={points}
+ lineCap="round"
+ stroke={TILE_PLUS_COLOR}
+ strokeWidth={TILE_PLUS_WIDTH_IN_PIXELS * scale}
+ listening={false}
+ />
+ ))}
+ </Group>
+ )
+}
+
+TilePlusIcon.propTypes = {
+ x: PropTypes.number,
+ y: PropTypes.number,
+ scale: PropTypes.number,
+}
+
+export default TilePlusIcon
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/WallSegment.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/WallSegment.js
new file mode 100644
index 00000000..4f18813e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/elements/WallSegment.js
@@ -0,0 +1,32 @@
+import React from 'react'
+import { Line } from 'react-konva'
+import { WallSegment as WallSegmentShape } from '../../../../shapes'
+import { WALL_COLOR } from '../../../../util/colors'
+import { TILE_SIZE_IN_PIXELS, WALL_WIDTH_IN_PIXELS } from '../MapConstants'
+
+function WallSegment({ wallSegment }) {
+ let points
+ if (wallSegment.isHorizontal) {
+ points = [
+ wallSegment.startPosX * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosY * TILE_SIZE_IN_PIXELS,
+ (wallSegment.startPosX + wallSegment.length) * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosY * TILE_SIZE_IN_PIXELS,
+ ]
+ } else {
+ points = [
+ wallSegment.startPosX * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosY * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosX * TILE_SIZE_IN_PIXELS,
+ (wallSegment.startPosY + wallSegment.length) * TILE_SIZE_IN_PIXELS,
+ ]
+ }
+
+ return <Line points={points} lineCap="round" stroke={WALL_COLOR} strokeWidth={WALL_WIDTH_IN_PIXELS} />
+}
+
+WallSegment.propTypes = {
+ wallSegment: WallSegmentShape,
+}
+
+export default WallSegment
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/GridGroup.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/GridGroup.js
new file mode 100644
index 00000000..d66a18de
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/GridGroup.js
@@ -0,0 +1,36 @@
+import React from 'react'
+import { Group, Line } from 'react-konva'
+import { GRID_COLOR } from '../../../../util/colors'
+import { GRID_LINE_WIDTH_IN_PIXELS, MAP_SIZE, MAP_SIZE_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+const MAP_COORDINATE_ENTRIES = Array.from(new Array(MAP_SIZE), (x, i) => i)
+const HORIZONTAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map((index) => [
+ 0,
+ index * TILE_SIZE_IN_PIXELS,
+ MAP_SIZE_IN_PIXELS,
+ index * TILE_SIZE_IN_PIXELS,
+])
+const VERTICAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map((index) => [
+ index * TILE_SIZE_IN_PIXELS,
+ 0,
+ index * TILE_SIZE_IN_PIXELS,
+ MAP_SIZE_IN_PIXELS,
+])
+
+function GridGroup() {
+ return (
+ <Group>
+ {HORIZONTAL_POINT_PAIRS.concat(VERTICAL_POINT_PAIRS).map((points, index) => (
+ <Line
+ key={index}
+ points={points}
+ stroke={GRID_COLOR}
+ strokeWidth={GRID_LINE_WIDTH_IN_PIXELS}
+ listening={false}
+ />
+ ))}
+ </Group>
+ )
+}
+
+export default GridGroup
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/RackGroup.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/RackGroup.js
new file mode 100644
index 00000000..ed942661
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/RackGroup.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import { Group } from 'react-konva'
+import { Tile } from '../../../../shapes'
+import { RACK_BACKGROUND_COLOR } from '../../../../util/colors'
+import TileObject from '../elements/TileObject'
+import RackSpaceFillContainer from '../RackSpaceFillContainer'
+import RackEnergyFillContainer from '../RackEnergyFillContainer'
+
+function RackGroup({ tile }) {
+ return (
+ <Group>
+ <TileObject positionX={tile.positionX} positionY={tile.positionY} color={RACK_BACKGROUND_COLOR} />
+ <Group>
+ <RackSpaceFillContainer rackId={tile.rack} positionX={tile.positionX} positionY={tile.positionY} />
+ <RackEnergyFillContainer rackId={tile.rack} positionX={tile.positionX} positionY={tile.positionY} />
+ </Group>
+ </Group>
+ )
+}
+
+RackGroup.propTypes = {
+ tile: Tile,
+}
+
+export default RackGroup
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/RoomGroup.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/RoomGroup.js
new file mode 100644
index 00000000..3f8b3089
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/RoomGroup.js
@@ -0,0 +1,52 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Group } from 'react-konva'
+import { InteractionLevel, Room } from '../../../../shapes'
+import GrayContainer from '../GrayContainer'
+import TileContainer from '../TileContainer'
+import WallContainer from '../WallContainer'
+
+function RoomGroup({ room, interactionLevel, currentRoomInConstruction, onClick }) {
+ if (currentRoomInConstruction === room.id) {
+ return (
+ <Group onClick={onClick}>
+ {room.tiles.map((tileId) => (
+ <TileContainer key={tileId} tileId={tileId} newTile={true} />
+ ))}
+ </Group>
+ )
+ }
+
+ return (
+ <Group onClick={onClick}>
+ {(() => {
+ if (
+ (interactionLevel.mode === 'RACK' || interactionLevel.mode === 'MACHINE') &&
+ interactionLevel.roomId === room.id
+ ) {
+ return [
+ room.tiles
+ .filter((tileId) => tileId !== interactionLevel.tileId)
+ .map((tileId) => <TileContainer key={tileId} tileId={tileId} />),
+ <GrayContainer key={-1} />,
+ room.tiles
+ .filter((tileId) => tileId === interactionLevel.tileId)
+ .map((tileId) => <TileContainer key={tileId} tileId={tileId} />),
+ ]
+ } else {
+ return room.tiles.map((tileId) => <TileContainer key={tileId} tileId={tileId} />)
+ }
+ })()}
+ <WallContainer roomId={room.id} />
+ </Group>
+ )
+}
+
+RoomGroup.propTypes = {
+ room: Room,
+ interactionLevel: InteractionLevel,
+ currentRoomInConstruction: PropTypes.string,
+ onClick: PropTypes.func,
+}
+
+export default RoomGroup
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/TileGroup.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/TileGroup.js
new file mode 100644
index 00000000..f2084017
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/TileGroup.js
@@ -0,0 +1,36 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Group } from 'react-konva'
+import { Tile } from '../../../../shapes'
+import { ROOM_DEFAULT_COLOR, ROOM_IN_CONSTRUCTION_COLOR } from '../../../../util/colors'
+import RoomTile from '../elements/RoomTile'
+import RackContainer from '../RackContainer'
+
+function TileGroup({ tile, newTile, onClick }) {
+ let tileObject
+ if (tile.rack) {
+ tileObject = <RackContainer tile={tile} />
+ } else {
+ tileObject = null
+ }
+
+ let color = ROOM_DEFAULT_COLOR
+ if (newTile) {
+ color = ROOM_IN_CONSTRUCTION_COLOR
+ }
+
+ return (
+ <Group onClick={() => onClick(tile)}>
+ <RoomTile tile={tile} color={color} />
+ {tileObject}
+ </Group>
+ )
+}
+
+TileGroup.propTypes = {
+ tile: Tile,
+ newTile: PropTypes.bool,
+ onClick: PropTypes.func,
+}
+
+export default TileGroup
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/TopologyGroup.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/TopologyGroup.js
new file mode 100644
index 00000000..011dcf34
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/TopologyGroup.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import { Group } from 'react-konva'
+import { InteractionLevel, Topology } from '../../../../shapes'
+import RoomContainer from '../RoomContainer'
+import GrayContainer from '../GrayContainer'
+
+function TopologyGroup({ topology, interactionLevel }) {
+ if (!topology) {
+ return <Group />
+ }
+
+ if (interactionLevel.mode === 'BUILDING') {
+ return (
+ <Group>
+ {topology.rooms.map((roomId) => (
+ <RoomContainer key={roomId} roomId={roomId} />
+ ))}
+ </Group>
+ )
+ }
+
+ return (
+ <Group>
+ {topology.rooms
+ .filter((roomId) => roomId !== interactionLevel.roomId)
+ .map((roomId) => (
+ <RoomContainer key={roomId} roomId={roomId} />
+ ))}
+ {interactionLevel.mode === 'ROOM' ? <GrayContainer /> : null}
+ {topology.rooms
+ .filter((roomId) => roomId === interactionLevel.roomId)
+ .map((roomId) => (
+ <RoomContainer key={roomId} roomId={roomId} />
+ ))}
+ </Group>
+ )
+}
+
+TopologyGroup.propTypes = {
+ topology: Topology,
+ interactionLevel: InteractionLevel,
+}
+
+export default TopologyGroup
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/WallGroup.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/WallGroup.js
new file mode 100644
index 00000000..6cbd1cd0
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/groups/WallGroup.js
@@ -0,0 +1,22 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Group } from 'react-konva'
+import { Tile } from '../../../../shapes'
+import { deriveWallLocations } from '../../../../util/tile-calculations'
+import WallSegment from '../elements/WallSegment'
+
+function WallGroup({ tiles }) {
+ return (
+ <Group>
+ {deriveWallLocations(tiles).map((wallSegment, index) => (
+ <WallSegment key={index} wallSegment={wallSegment} />
+ ))}
+ </Group>
+ )
+}
+
+WallGroup.propTypes = {
+ tiles: PropTypes.arrayOf(Tile).isRequired,
+}
+
+export default WallGroup
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/HoverLayerComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/HoverLayerComponent.js
new file mode 100644
index 00000000..d7e0c56a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/HoverLayerComponent.js
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types'
+import React, { useMemo, useState } from 'react'
+import { Layer } from 'react-konva/lib/ReactKonva'
+import HoverTile from '../elements/HoverTile'
+import { TILE_SIZE_IN_PIXELS } from '../MapConstants'
+import { useEffectRef } from '../../../../util/effect-ref'
+
+function HoverLayerComponent({ isEnabled, isValid, onClick, children }) {
+ const [[mouseWorldX, mouseWorldY], setPos] = useState([0, 0])
+
+ const layerRef = useEffectRef((layer) => {
+ if (!layer) {
+ return
+ }
+
+ const stage = layer.getStage()
+
+ stage.on('mousemove.hover', () => {
+ // Transform used to convert mouse coordinates to world coordinates
+ const transform = stage.getAbsoluteTransform().copy()
+ transform.invert()
+
+ const { x, y } = transform.point(stage.getPointerPosition())
+ setPos([x, y])
+ })
+ return () => stage.off('mousemove.hover')
+ })
+
+ const gridX = Math.floor(mouseWorldX / TILE_SIZE_IN_PIXELS)
+ const gridY = Math.floor(mouseWorldY / TILE_SIZE_IN_PIXELS)
+ const valid = useMemo(() => isEnabled && isValid(gridX, gridY), [isEnabled, isValid, gridX, gridY])
+
+ if (!isEnabled) {
+ return <Layer />
+ }
+
+ const x = gridX * TILE_SIZE_IN_PIXELS
+ const y = gridY * TILE_SIZE_IN_PIXELS
+
+ return (
+ <Layer opacity={0.2} ref={layerRef}>
+ <HoverTile x={x} y={y} isValid={valid} onClick={() => (valid ? onClick(gridX, gridY) : undefined)} />
+ {children ? React.cloneElement(children, { x, y, scale: 1 }) : undefined}
+ </Layer>
+ )
+}
+
+HoverLayerComponent.propTypes = {
+ isEnabled: PropTypes.bool.isRequired,
+ isValid: PropTypes.func.isRequired,
+ onClick: PropTypes.func.isRequired,
+ children: PropTypes.node,
+}
+
+export default HoverLayerComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/MapLayer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/MapLayer.js
new file mode 100644
index 00000000..c902532b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/MapLayer.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { Group, Layer } from 'react-konva'
+import Backdrop from '../elements/Backdrop'
+import TopologyContainer from '../TopologyContainer'
+import GridGroup from '../groups/GridGroup'
+
+function MapLayer() {
+ return (
+ <Layer>
+ <Group>
+ <Backdrop />
+ <TopologyContainer />
+ <GridGroup />
+ </Group>
+ </Layer>
+ )
+}
+
+export default MapLayer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/ObjectHoverLayer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/ObjectHoverLayer.js
new file mode 100644
index 00000000..5e741a3b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/ObjectHoverLayer.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { addRackToTile } from '../../../../redux/actions/topology/room'
+import { findTileWithPosition } from '../../../../util/tile-calculations'
+import HoverLayerComponent from './HoverLayerComponent'
+import TilePlusIcon from '../elements/TilePlusIcon'
+
+export default function ObjectHoverLayer() {
+ const isEnabled = useSelector((state) => state.construction.inRackConstructionMode)
+ const isValid = useSelector((state) => (x, y) => {
+ if (state.interactionLevel.mode !== 'ROOM') {
+ return false
+ }
+
+ const currentRoom = state.topology.rooms[state.interactionLevel.roomId]
+ const tiles = currentRoom.tiles.map((tileId) => state.topology.tiles[tileId])
+ const tile = findTileWithPosition(tiles, x, y)
+
+ return !(tile === null || tile.rack)
+ })
+
+ const dispatch = useDispatch()
+ const onClick = (x, y) => dispatch(addRackToTile(x, y))
+ return (
+ <HoverLayerComponent onClick={onClick} isEnabled={isEnabled} isValid={isValid}>
+ <TilePlusIcon />
+ </HoverLayerComponent>
+ )
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/RoomHoverLayer.js
new file mode 100644
index 00000000..b9cfcaf4
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/map/layers/RoomHoverLayer.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { toggleTileAtLocation } from '../../../../redux/actions/topology/building'
+import {
+ deriveValidNextTilePositions,
+ findPositionInPositions,
+ findPositionInRooms,
+} from '../../../../util/tile-calculations'
+import HoverLayerComponent from './HoverLayerComponent'
+
+export default function RoomHoverLayer() {
+ const dispatch = useDispatch()
+ const onClick = (x, y) => dispatch(toggleTileAtLocation(x, y))
+ const isEnabled = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
+ const isValid = useSelector((state) => (x, y) => {
+ const newRoom = { ...state.topology.rooms[state.construction.currentRoomInConstruction] }
+ const oldRooms = Object.keys(state.topology.rooms)
+ .map((id) => ({ ...state.topology.rooms[id] }))
+ .filter(
+ (room) =>
+ state.topology.root.rooms.indexOf(room.id) !== -1 &&
+ room.id !== state.construction.currentRoomInConstruction
+ )
+
+ ;[...oldRooms, newRoom].forEach((room) => {
+ room.tiles = room.tiles.map((tileId) => state.topology.tiles[tileId])
+ })
+ if (newRoom.tiles.length === 0) {
+ return findPositionInRooms(oldRooms, x, y) === -1
+ }
+
+ const validNextPositions = deriveValidNextTilePositions(oldRooms, newRoom.tiles)
+ return findPositionInPositions(validNextPositions, x, y) !== -1
+ })
+
+ return <HoverLayerComponent onClick={onClick} isEnabled={isEnabled} isValid={isValid} />
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/NameComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/NameComponent.js
new file mode 100644
index 00000000..ececd07b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/NameComponent.js
@@ -0,0 +1,69 @@
+import PropTypes from 'prop-types'
+import React, { useRef, useState } from 'react'
+import { Button, TextInput } from '@patternfly/react-core'
+import { PencilAltIcon, CheckIcon, TimesIcon } from '@patternfly/react-icons'
+
+function NameComponent({ name, onEdit }) {
+ const [isEditing, setEditing] = useState(false)
+ const nameInput = useRef(null)
+
+ const onCancel = () => {
+ nameInput.current.value = name
+ setEditing(false)
+ }
+
+ const onSubmit = (event) => {
+ if (event) {
+ event.preventDefault()
+ }
+
+ const name = nameInput.current.value
+ if (name) {
+ onEdit(name)
+ }
+
+ setEditing(false)
+ }
+
+ return (
+ <form
+ className={`pf-c-inline-edit ${isEditing ? 'pf-m-inline-editable' : ''} pf-u-display-inline-block`}
+ onSubmit={onSubmit}
+ >
+ <div className="pf-c-inline-edit__group">
+ <div className="pf-c-inline-edit__value" id="single-inline-edit-example-label">
+ {name}
+ </div>
+ <div className="pf-c-inline-edit__action pf-m-enable-editable">
+ <Button className="pf-u-py-0" variant="plain" aria-label="Edit" onClick={() => setEditing(true)}>
+ <PencilAltIcon />
+ </Button>
+ </div>
+ </div>
+ <div className="pf-c-inline-edit__group">
+ <div className="pf-c-inline-edit__input">
+ <TextInput type="text" defaultValue={name} ref={nameInput} aria-label="Editable text input" />
+ </div>
+ <div className="pf-c-inline-edit__group pf-m-action-group pf-m-icon-group">
+ <div className="pf-c-inline-edit__action pf-m-valid">
+ <Button className="pf-u-py-0" variant="plain" aria-label="Save edits" onClick={onSubmit}>
+ <CheckIcon />
+ </Button>
+ </div>
+ <div className="pf-c-inline-edit__action">
+ <Button className="pf-u-py-0" variant="plain" aria-label="Cancel edits" onClick={onCancel}>
+ <TimesIcon />
+ </Button>
+ </div>
+ </div>
+ </div>
+ </form>
+ )
+}
+
+NameComponent.propTypes = {
+ name: PropTypes.string,
+ onEdit: PropTypes.func,
+}
+
+export default NameComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.js
new file mode 100644
index 00000000..5aaa7834
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.js
@@ -0,0 +1,83 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { InteractionLevel } from '../../../shapes'
+import BuildingSidebar from './building/BuildingSidebar'
+import {
+ Button,
+ DrawerActions,
+ DrawerCloseButton,
+ DrawerHead,
+ DrawerPanelBody,
+ DrawerPanelContent,
+ Flex,
+ Title,
+} from '@patternfly/react-core'
+import { AngleLeftIcon } from '@patternfly/react-icons'
+import { useDispatch } from 'react-redux'
+import { backButton } from './TopologySidebar.module.css'
+import RoomSidebar from './room/RoomSidebar'
+import RackSidebar from './rack/RackSidebar'
+import MachineSidebar from './machine/MachineSidebar'
+import { goDownOneInteractionLevel } from '../../../redux/actions/interaction-level'
+
+const name = {
+ BUILDING: 'Building',
+ ROOM: 'Room',
+ RACK: 'Rack',
+ MACHINE: 'Machine',
+}
+
+function TopologySidebar({ interactionLevel, onClose }) {
+ let sidebarContent
+
+ switch (interactionLevel.mode) {
+ case 'BUILDING':
+ sidebarContent = <BuildingSidebar />
+ break
+ case 'ROOM':
+ sidebarContent = <RoomSidebar roomId={interactionLevel.roomId} />
+ break
+ case 'RACK':
+ sidebarContent = <RackSidebar tileId={interactionLevel.tileId} />
+ break
+ case 'MACHINE':
+ sidebarContent = <MachineSidebar tileId={interactionLevel.tileId} position={interactionLevel.position} />
+ break
+ default:
+ sidebarContent = 'Missing Content'
+ }
+
+ const dispatch = useDispatch()
+ const onClick = () => dispatch(goDownOneInteractionLevel())
+
+ return (
+ <DrawerPanelContent isResizable defaultSize="450px" minSize="400px">
+ <DrawerHead>
+ <Flex>
+ <Button
+ variant="tertiary"
+ isSmall
+ className={backButton}
+ onClick={interactionLevel.mode === 'BUILDING' ? onClose : onClick}
+ >
+ <AngleLeftIcon />
+ </Button>
+ <Title className="pf-u-align-self-center" headingLevel="h1">
+ {name[interactionLevel.mode]}
+ </Title>
+ </Flex>
+ <DrawerActions>
+ <DrawerCloseButton onClose={onClose} />
+ </DrawerActions>
+ </DrawerHead>
+ <DrawerPanelBody>{sidebarContent}</DrawerPanelBody>
+ </DrawerPanelContent>
+ )
+}
+
+TopologySidebar.propTypes = {
+ interactionLevel: InteractionLevel,
+ onClose: PropTypes.func,
+}
+
+export default TopologySidebar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.module.css b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.module.css
new file mode 100644
index 00000000..3853c625
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.module.css
@@ -0,0 +1,35 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.backButton:global(.pf-c-button) {
+ align-self: center;
+ --pf-c-button--after--BorderColor: var(--pf-global--BorderColor--light-100);
+ color: var(--pf-global--Color--400);
+
+ --pf-c-button--PaddingRight: var(--pf-global--spacer--sm);
+ --pf-c-button--PaddingLeft: var(--pf-global--spacer--sm);
+}
+
+.backButton:hover,
+.backButton:global(.pf-c-button):focus {
+ --pf-c-button--after--BorderColor: var(--pf-global--BorderColor--100);
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/BuildingSidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/BuildingSidebar.js
new file mode 100644
index 00000000..5fcd46be
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/BuildingSidebar.js
@@ -0,0 +1,8 @@
+import React from 'react'
+import NewRoomConstructionContainer from './NewRoomConstructionContainer'
+
+function BuildingSidebar() {
+ return <NewRoomConstructionContainer />
+}
+
+export default BuildingSidebar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionComponent.js
new file mode 100644
index 00000000..9fc85d0c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionComponent.js
@@ -0,0 +1,46 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Button, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'
+import PlusIcon from '@patternfly/react-icons/dist/js/icons/plus-icon'
+import CheckIcon from '@patternfly/react-icons/dist/js/icons/check-icon'
+
+function NewRoomConstructionComponent({ onStart, onFinish, onCancel, currentRoomInConstruction }) {
+ if (currentRoomInConstruction === '-1') {
+ return (
+ <Button isBlock icon={<PlusIcon />} onClick={onStart}>
+ Construct a new room
+ </Button>
+ )
+ }
+ return (
+ <Toolbar
+ inset={{
+ default: 'insetNone',
+ }}
+ >
+ <ToolbarContent>
+ <ToolbarGroup>
+ <ToolbarItem>
+ <Button icon={<CheckIcon />} onClick={onFinish}>
+ Finalize new room
+ </Button>
+ </ToolbarItem>
+ <ToolbarItem widths={{ default: '100%' }}>
+ <Button isBlock variant="secondary" onClick={onCancel}>
+ Cancel
+ </Button>
+ </ToolbarItem>
+ </ToolbarGroup>
+ </ToolbarContent>
+ </Toolbar>
+ )
+}
+
+NewRoomConstructionComponent.propTypes = {
+ onStart: PropTypes.func,
+ onFinish: PropTypes.func,
+ onCancel: PropTypes.func,
+ currentRoomInConstruction: PropTypes.string,
+}
+
+export default NewRoomConstructionComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionContainer.js
new file mode 100644
index 00000000..c149b224
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionContainer.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import {
+ cancelNewRoomConstruction,
+ finishNewRoomConstruction,
+ startNewRoomConstruction,
+} from '../../../../redux/actions/topology/building'
+import NewRoomConstructionComponent from './NewRoomConstructionComponent'
+
+function NewRoomConstructionButton() {
+ const currentRoomInConstruction = useSelector((state) => state.construction.currentRoomInConstruction)
+ const dispatch = useDispatch()
+
+ return (
+ <NewRoomConstructionComponent
+ onStart={() => dispatch(startNewRoomConstruction())}
+ onFinish={() => dispatch(finishNewRoomConstruction())}
+ onCancel={() => dispatch(cancelNewRoomConstruction())}
+ currentRoomInConstruction={currentRoomInConstruction}
+ />
+ )
+}
+
+export default NewRoomConstructionButton
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/DeleteMachine.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/DeleteMachine.js
new file mode 100644
index 00000000..a4b9457b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/DeleteMachine.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React, { useState } from 'react'
+import { useDispatch } from 'react-redux'
+import { Button } from '@patternfly/react-core'
+import { TrashIcon } from '@patternfly/react-icons'
+import ConfirmationModal from '../../../util/modals/ConfirmationModal'
+import { deleteMachine } from '../../../../redux/actions/topology/machine'
+
+function DeleteMachine({ machineId }) {
+ const dispatch = useDispatch()
+ const [isVisible, setVisible] = useState(false)
+ const callback = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteMachine(machineId))
+ }
+ setVisible(false)
+ }
+ return (
+ <>
+ <Button variant="danger" icon={<TrashIcon />} isBlock onClick={() => setVisible(true)}>
+ Delete this machine
+ </Button>
+ <ConfirmationModal
+ title="Delete this machine"
+ message="Are you sure you want to delete this machine?"
+ isOpen={isVisible}
+ callback={callback}
+ />
+ </>
+ )
+}
+
+DeleteMachine.propTypes = {
+ machineId: PropTypes.string.isRequired,
+}
+
+export default DeleteMachine
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/MachineSidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/MachineSidebar.js
new file mode 100644
index 00000000..8a4c33dc
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/MachineSidebar.js
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import UnitTabsComponent from './UnitTabsComponent'
+import DeleteMachine from './DeleteMachine'
+import {
+ TextContent,
+ TextList,
+ TextListItem,
+ TextListItemVariants,
+ TextListVariants,
+ Title,
+} from '@patternfly/react-core'
+import { useSelector } from 'react-redux'
+
+function MachineSidebar({ tileId, position }) {
+ const machine = useSelector(({ topology }) => {
+ const rack = topology.racks[topology.tiles[tileId].rack]
+
+ for (const machineId of rack.machines) {
+ const machine = topology.machines[machineId]
+ if (machine.position === position) {
+ return machine
+ }
+ }
+ })
+ const machineId = machine.id
+ return (
+ <div>
+ <TextContent>
+ <Title headingLevel="h2">Details</Title>
+ <TextList component={TextListVariants.dl}>
+ <TextListItem component={TextListItemVariants.dt}>Name</TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>
+ Machine at position {machine.position}
+ </TextListItem>
+ </TextList>
+
+ <Title headingLevel="h2">Actions</Title>
+ <DeleteMachine machineId={machineId} />
+
+ <Title headingLevel="h2">Units</Title>
+ </TextContent>
+ <div className="pf-u-h-100">
+ <UnitTabsComponent machineId={machineId} />
+ </div>
+ </div>
+ )
+}
+
+MachineSidebar.propTypes = {
+ tileId: PropTypes.string.isRequired,
+ position: PropTypes.number.isRequired,
+}
+
+export default MachineSidebar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddComponent.js
new file mode 100644
index 00000000..18cba23a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddComponent.js
@@ -0,0 +1,42 @@
+import PropTypes from 'prop-types'
+import React, { useState } from 'react'
+import { Button, InputGroup, Select, SelectOption, SelectVariant } from '@patternfly/react-core'
+import PlusIcon from '@patternfly/react-icons/dist/js/icons/plus-icon'
+
+function UnitAddComponent({ units, onAdd }) {
+ const [isOpen, setOpen] = useState(false)
+ const [selected, setSelected] = useState(null)
+
+ return (
+ <InputGroup>
+ <Select
+ variant={SelectVariant.single}
+ placeholderText="Select a unit"
+ aria-label="Select Unit"
+ onToggle={() => setOpen(!isOpen)}
+ isOpen={isOpen}
+ onSelect={(_, selection) => {
+ setSelected(selection)
+ setOpen(false)
+ }}
+ selections={selected}
+ >
+ {units.map((unit) => (
+ <SelectOption value={unit.id} key={unit.id}>
+ {unit.name}
+ </SelectOption>
+ ))}
+ </Select>
+ <Button icon={<PlusIcon />} variant="control" onClick={() => onAdd(selected)} isDisabled={!selected}>
+ Add
+ </Button>
+ </InputGroup>
+ )
+}
+
+UnitAddComponent.propTypes = {
+ units: PropTypes.array.isRequired,
+ onAdd: PropTypes.func.isRequired,
+}
+
+export default UnitAddComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddContainer.js
new file mode 100644
index 00000000..a0054ef6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddContainer.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import UnitAddComponent from './UnitAddComponent'
+import { addUnit } from '../../../../redux/actions/topology/machine'
+import UnitType from './UnitType'
+
+function UnitAddContainer({ machineId, unitType }) {
+ const units = useSelector((state) => Object.values(state.topology[unitType]))
+ const dispatch = useDispatch()
+
+ const onAdd = (id) => dispatch(addUnit(machineId, unitType, id))
+
+ return <UnitAddComponent onAdd={onAdd} units={units} />
+}
+
+UnitAddContainer.propTypes = {
+ machineId: PropTypes.string.isRequired,
+ unitType: UnitType.isRequired,
+}
+
+export default UnitAddContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListComponent.js
new file mode 100644
index 00000000..75ab0ad7
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListComponent.js
@@ -0,0 +1,113 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import {
+ Button,
+ DataList,
+ DataListAction,
+ DataListCell,
+ DataListItem,
+ DataListItemCells,
+ DataListItemRow,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ Popover,
+ Title,
+} from '@patternfly/react-core'
+import { CubesIcon, InfoIcon, TrashIcon } from '@patternfly/react-icons'
+import { ProcessingUnit, StorageUnit } from '../../../../shapes'
+import UnitType from './UnitType'
+
+function UnitInfo({ unit, unitType }) {
+ if (unitType === 'cpus' || unitType === 'gpus') {
+ return (
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Clock Frequency</DescriptionListTerm>
+ <DescriptionListDescription>{unit.clockRateMhz} MHz</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Number of Cores</DescriptionListTerm>
+ <DescriptionListDescription>{unit.numberOfCores}</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Energy Consumption</DescriptionListTerm>
+ <DescriptionListDescription>{unit.energyConsumptionW} W</DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ )
+ }
+
+ return (
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Speed</DescriptionListTerm>
+ <DescriptionListDescription>{unit.speedMbPerS} Mb/s</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Capacity</DescriptionListTerm>
+ <DescriptionListDescription>{unit.sizeMb} MB</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Energy Consumption</DescriptionListTerm>
+ <DescriptionListDescription>{unit.energyConsumptionW} W</DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ )
+}
+
+UnitInfo.propTypes = {
+ unitType: UnitType.isRequired,
+ unit: PropTypes.oneOfType([ProcessingUnit, StorageUnit]).isRequired,
+}
+
+function UnitListComponent({ unitType, units, onDelete }) {
+ if (units.length === 0) {
+ return (
+ <EmptyState>
+ <EmptyStateIcon icon={CubesIcon} />
+ <Title headingLevel="h5" size="lg">
+ No units found
+ </Title>
+ <EmptyStateBody>You have not configured any units yet. Add some with the menu above!</EmptyStateBody>
+ </EmptyState>
+ )
+ }
+
+ return (
+ <DataList aria-label="Machine Units" isCompact>
+ {units.map((unit, index) => (
+ <DataListItem key={index}>
+ <DataListItemRow>
+ <DataListItemCells dataListCells={[<DataListCell key="unit">{unit.name}</DataListCell>]} />
+ <DataListAction id="goto" aria-label="Goto Machine" aria-labelledby="goto">
+ <Popover
+ headerContent="Unit Information"
+ bodyContent={<UnitInfo unitType={unitType} unit={unit} />}
+ >
+ <Button isSmall variant="plain" className="pf-u-p-0">
+ <InfoIcon />
+ </Button>
+ </Popover>
+ <Button isSmall variant="plain" className="pf-u-p-0" onClick={() => onDelete(units[index])}>
+ <TrashIcon />
+ </Button>
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ ))}
+ </DataList>
+ )
+}
+
+UnitListComponent.propTypes = {
+ unitType: UnitType.isRequired,
+ units: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, StorageUnit])).isRequired,
+ onDelete: PropTypes.func,
+}
+
+export default UnitListComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListContainer.js
new file mode 100644
index 00000000..bcd4bdcc
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListContainer.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import UnitListComponent from './UnitListComponent'
+import { deleteUnit } from '../../../../redux/actions/topology/machine'
+import UnitType from './UnitType'
+
+function UnitListContainer({ machineId, unitType }) {
+ const dispatch = useDispatch()
+ const units = useSelector((state) => {
+ const machine = state.topology.machines[machineId]
+ return machine[unitType].map((id) => state.topology[unitType][id])
+ })
+
+ const onDelete = (unit) => dispatch(deleteUnit(machineId, unitType, unit.id))
+
+ return <UnitListComponent units={units} unitType={unitType} onDelete={onDelete} />
+}
+
+UnitListContainer.propTypes = {
+ machineId: PropTypes.string.isRequired,
+ unitType: UnitType.isRequired,
+}
+
+export default UnitListContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitTabsComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitTabsComponent.js
new file mode 100644
index 00000000..4032d607
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitTabsComponent.js
@@ -0,0 +1,36 @@
+import PropTypes from 'prop-types'
+import React, { useState } from 'react'
+import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'
+import UnitAddContainer from './UnitAddContainer'
+import UnitListContainer from './UnitListContainer'
+
+function UnitTabsComponent({ machineId }) {
+ const [activeTab, setActiveTab] = useState('cpuModel-units')
+
+ return (
+ <Tabs activeKey={activeTab} onSelect={(_, tab) => setActiveTab(tab)}>
+ <Tab eventKey="cpuModel-units" title={<TabTitleText>CPU</TabTitleText>}>
+ <UnitAddContainer machineId={machineId} unitType="cpus" />
+ <UnitListContainer machineId={machineId} unitType="cpus" />
+ </Tab>
+ <Tab eventKey="gpu-units" title={<TabTitleText>GPU</TabTitleText>}>
+ <UnitAddContainer machineId={machineId} unitType="gpus" />
+ <UnitListContainer machineId={machineId} unitType="gpus" />
+ </Tab>
+ <Tab eventKey="memory-units" title={<TabTitleText>Memory</TabTitleText>}>
+ <UnitAddContainer machineId={machineId} unitType="memories" />
+ <UnitListContainer machineId={machineId} unitType="memories" />
+ </Tab>
+ <Tab eventKey="storage-units" title={<TabTitleText>Storage</TabTitleText>}>
+ <UnitAddContainer machineId={machineId} unitType="storages" />
+ <UnitListContainer machineId={machineId} unitType="storages" />
+ </Tab>
+ </Tabs>
+ )
+}
+
+UnitTabsComponent.propTypes = {
+ machineId: PropTypes.string.isRequired,
+}
+
+export default UnitTabsComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitType.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitType.js
new file mode 100644
index 00000000..b6d7bf8b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitType.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+
+export default PropTypes.oneOf(['cpus', 'gpus', 'memories', 'storages'])
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js
new file mode 100644
index 00000000..6a0c3ff3
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Button } from '@patternfly/react-core'
+import { SaveIcon } from '@patternfly/react-icons'
+
+function AddPrefab() {
+ const onClick = () => {} // TODO
+ return (
+ <Button variant="primary" icon={<SaveIcon />} isBlock onClick={onClick} className="pf-u-mb-sm">
+ Save this rack to a prefab
+ </Button>
+ )
+}
+
+AddPrefab.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default AddPrefab
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/DeleteRackContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/DeleteRackContainer.js
new file mode 100644
index 00000000..0583a7a4
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/DeleteRackContainer.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React, { useState } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon'
+import { Button } from '@patternfly/react-core'
+import ConfirmationModal from '../../../util/modals/ConfirmationModal'
+import { deleteRack } from '../../../../redux/actions/topology/rack'
+
+function DeleteRackContainer({ tileId }) {
+ const dispatch = useDispatch()
+ const [isVisible, setVisible] = useState(false)
+ const rackId = useSelector((state) => state.topology.tiles[tileId].rack)
+ const callback = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteRack(tileId, rackId))
+ }
+ setVisible(false)
+ }
+ return (
+ <>
+ <Button variant="danger" icon={<TrashIcon />} isBlock onClick={() => setVisible(true)}>
+ Delete this rack
+ </Button>
+ <ConfirmationModal
+ title="Delete this rack"
+ message="Are you sure you want to delete this rack?"
+ isOpen={isVisible}
+ callback={callback}
+ />
+ </>
+ )
+}
+
+DeleteRackContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default DeleteRackContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineComponent.js
new file mode 100644
index 00000000..b0a96a9f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineComponent.js
@@ -0,0 +1,40 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Flex, Label } from '@patternfly/react-core'
+import { Machine } from '../../../../shapes'
+
+const UnitIcon = ({ id, type }) => (
+ // eslint-disable-next-line @next/next/no-img-element
+ <img src={'/img/topology/' + id + '-icon.png'} alt={'Machine contains ' + type + ' units'} height={24} width={24} />
+)
+
+UnitIcon.propTypes = {
+ id: PropTypes.string,
+ type: PropTypes.string,
+}
+
+function MachineComponent({ machine, onClick }) {
+ const hasNoUnits =
+ machine.cpus.length + machine.gpus.length + machine.memories.length + machine.storages.length === 0
+
+ return (
+ <Flex onClick={() => onClick()}>
+ {machine.cpus.length > 0 ? <UnitIcon id="cpuModel" type="CPU" /> : undefined}
+ {machine.gpus.length > 0 ? <UnitIcon id="gpu" type="GPU" /> : undefined}
+ {machine.memories.length > 0 ? <UnitIcon id="memory" type="memory" /> : undefined}
+ {machine.storages.length > 0 ? <UnitIcon id="storage" type="storage" /> : undefined}
+ {hasNoUnits ? (
+ <Label variant="outline" color="orange">
+ Machine with no units
+ </Label>
+ ) : undefined}
+ </Flex>
+ )
+}
+
+MachineComponent.propTypes = {
+ machine: Machine.isRequired,
+ onClick: PropTypes.func,
+}
+
+export default MachineComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListComponent.js
new file mode 100644
index 00000000..02c97730
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListComponent.js
@@ -0,0 +1,80 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import MachineComponent from './MachineComponent'
+import {
+ Badge,
+ Button,
+ DataList,
+ DataListAction,
+ DataListCell,
+ DataListItem,
+ DataListItemCells,
+ DataListItemRow,
+} from '@patternfly/react-core'
+import { AngleRightIcon, PlusIcon } from '@patternfly/react-icons'
+import { Machine } from '../../../../shapes'
+
+function MachineListComponent({ machines = [], onSelect, onAdd }) {
+ return (
+ <DataList aria-label="Rack Units">
+ {machines
+ .map((machine, index) =>
+ machine ? (
+ <DataListItem key={index} onClick={() => onSelect(index + 1)}>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell isIcon key="icon">
+ <Badge isRead>{index + 1}U</Badge>
+ </DataListCell>,
+ <DataListCell key="primary content">
+ <MachineComponent onClick={() => onSelect(index + 1)} machine={machine} />
+ </DataListCell>,
+ ]}
+ />
+ <DataListAction id="goto" aria-label="Goto Machine" aria-labelledby="goto">
+ <Button isSmall variant="plain" className="pf-u-p-0">
+ <AngleRightIcon />
+ </Button>
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ ) : (
+ <DataListItem key={index}>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell isIcon key="icon">
+ <Badge isRead>{index + 1}U</Badge>
+ </DataListCell>,
+ <DataListCell key="add" className="text-secondary">
+ Empty Slot
+ </DataListCell>,
+ ]}
+ />
+ <DataListAction id="add" aria-label="Add Machine" aria-labelledby="add">
+ <Button
+ isSmall
+ variant="plain"
+ className="pf-u-p-0"
+ onClick={() => onAdd(index + 1)}
+ >
+ <PlusIcon />
+ </Button>
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ )
+ )
+ .reverse()}
+ </DataList>
+ )
+}
+
+MachineListComponent.propTypes = {
+ machines: PropTypes.arrayOf(Machine),
+ onSelect: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired,
+}
+
+export default MachineListComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListContainer.js
new file mode 100644
index 00000000..e1914730
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListContainer.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React, { useMemo } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import MachineListComponent from './MachineListComponent'
+import { goFromRackToMachine } from '../../../../redux/actions/interaction-level'
+import { addMachine } from '../../../../redux/actions/topology/rack'
+
+function MachineListContainer({ tileId, ...props }) {
+ const rack = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack])
+ const machines = useSelector((state) => rack.machines.map((id) => state.topology.machines[id]))
+ const machinesNull = useMemo(() => {
+ const res = Array(rack.capacity).fill(null)
+ for (const machine of machines) {
+ res[machine.position - 1] = machine
+ }
+ return res
+ }, [rack, machines])
+ const dispatch = useDispatch()
+
+ return (
+ <MachineListComponent
+ {...props}
+ machines={machinesNull}
+ onAdd={(index) => dispatch(addMachine(rack.id, index))}
+ onSelect={(index) => dispatch(goFromRackToMachine(index))}
+ />
+ )
+}
+
+MachineListContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default MachineListContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackNameContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackNameContainer.js
new file mode 100644
index 00000000..c3422318
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackNameContainer.js
@@ -0,0 +1,22 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import NameComponent from '../NameComponent'
+import { editRackName } from '../../../../redux/actions/topology/rack'
+
+const RackNameContainer = ({ tileId }) => {
+ const { name: rackName, id } = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack])
+ const dispatch = useDispatch()
+ const callback = (name) => {
+ if (name) {
+ dispatch(editRackName(id, name))
+ }
+ }
+ return <NameComponent name={rackName} onEdit={callback} />
+}
+
+RackNameContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default RackNameContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.js
new file mode 100644
index 00000000..cb7d3b68
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.js
@@ -0,0 +1,58 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { machineListContainer, sidebarContainer } from './RackSidebar.module.css'
+import RackNameContainer from './RackNameContainer'
+import AddPrefab from './AddPrefab'
+import DeleteRackContainer from './DeleteRackContainer'
+import MachineListContainer from './MachineListContainer'
+import {
+ Skeleton,
+ TextContent,
+ TextList,
+ TextListItem,
+ TextListItemVariants,
+ TextListVariants,
+ Title,
+} from '@patternfly/react-core'
+import { useSelector } from 'react-redux'
+
+function RackSidebar({ tileId }) {
+ const rack = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack])
+
+ return (
+ <div className={sidebarContainer}>
+ <TextContent>
+ <Title headingLevel="h2">Details</Title>
+ <TextList component={TextListVariants.dl}>
+ <TextListItem
+ component={TextListItemVariants.dt}
+ className="pf-u-display-inline-flex pf-u-align-items-center"
+ >
+ Name
+ </TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>
+ <RackNameContainer tileId={tileId} />
+ </TextListItem>
+ <TextListItem component={TextListItemVariants.dt}>Capacity</TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>
+ {rack?.capacity ?? <Skeleton screenreaderText="Loading rack" />}
+ </TextListItem>
+ </TextList>
+ <Title headingLevel="h2">Actions</Title>
+ <AddPrefab tileId={tileId} />
+ <DeleteRackContainer tileId={tileId} />
+
+ <Title headingLevel="h2">Slots</Title>
+ </TextContent>
+ <div className={machineListContainer}>
+ <MachineListContainer tileId={tileId} />
+ </div>
+ </div>
+ )
+}
+
+RackSidebar.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default RackSidebar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.module.css b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.module.css
new file mode 100644
index 00000000..f4c8829f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.module.css
@@ -0,0 +1,14 @@
+.sidebarContainer {
+ display: flex;
+ flex-direction: column;
+
+ height: 100%;
+}
+
+.machineListContainer {
+ overflow-y: auto;
+
+ flex: 1 0 300px;
+
+ margin-top: 10px;
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/DeleteRoomContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/DeleteRoomContainer.js
new file mode 100644
index 00000000..29b8f78a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/DeleteRoomContainer.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React, { useState } from 'react'
+import { useDispatch } from 'react-redux'
+import ConfirmationModal from '../../../util/modals/ConfirmationModal'
+import { deleteRoom } from '../../../../redux/actions/topology/room'
+import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon'
+import { Button } from '@patternfly/react-core'
+
+function DeleteRoomContainer({ roomId }) {
+ const dispatch = useDispatch()
+ const [isVisible, setVisible] = useState(false)
+ const callback = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteRoom(roomId))
+ }
+ setVisible(false)
+ }
+ return (
+ <>
+ <Button variant="danger" icon={<TrashIcon />} isBlock onClick={() => setVisible(true)}>
+ Delete this room
+ </Button>
+ <ConfirmationModal
+ title="Delete this room"
+ message="Are you sure you want to delete this room?"
+ isOpen={isVisible}
+ callback={callback}
+ />
+ </>
+ )
+}
+
+DeleteRoomContainer.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default DeleteRoomContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/EditRoomContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/EditRoomContainer.js
new file mode 100644
index 00000000..7a278cd6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/EditRoomContainer.js
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { finishRoomEdit, startRoomEdit } from '../../../../redux/actions/topology/building'
+import CheckIcon from '@patternfly/react-icons/dist/js/icons/check-icon'
+import PencilAltIcon from '@patternfly/react-icons/dist/js/icons/pencil-alt-icon'
+import { Button } from '@patternfly/react-core'
+
+function EditRoomContainer({ roomId }) {
+ const isEditing = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
+ const isInRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode)
+
+ const dispatch = useDispatch()
+ const onEdit = () => dispatch(startRoomEdit(roomId))
+ const onFinish = () => dispatch(finishRoomEdit())
+
+ return isEditing ? (
+ <Button variant="tertiary" icon={<CheckIcon />} isBlock onClick={onFinish} className="pf-u-mb-sm">
+ Finish editing room
+ </Button>
+ ) : (
+ <Button
+ variant="tertiary"
+ icon={<PencilAltIcon />}
+ isBlock
+ disabled={isInRackConstructionMode}
+ onClick={() => (isInRackConstructionMode ? undefined : onEdit())}
+ className="pf-u-mb-sm"
+ >
+ Edit the tiles of this room
+ </Button>
+ )
+}
+
+EditRoomContainer.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default EditRoomContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js
new file mode 100644
index 00000000..a384d5d5
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js
@@ -0,0 +1,35 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Button } from '@patternfly/react-core'
+import { PlusIcon, TimesIcon } from '@patternfly/react-icons'
+
+const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, isEditingRoom }) => {
+ if (inRackConstructionMode) {
+ return (
+ <Button isBlock={true} icon={<TimesIcon />} onClick={onStop} className="pf-u-mb-sm">
+ Stop rack construction
+ </Button>
+ )
+ }
+
+ return (
+ <Button
+ icon={<PlusIcon />}
+ isBlock
+ isDisabled={isEditingRoom}
+ onClick={() => (isEditingRoom ? undefined : onStart())}
+ className="pf-u-mb-sm"
+ >
+ Start rack construction
+ </Button>
+ )
+}
+
+RackConstructionComponent.propTypes = {
+ onStart: PropTypes.func,
+ onStop: PropTypes.func,
+ inRackConstructionMode: PropTypes.bool,
+ isEditingRoom: PropTypes.bool,
+}
+
+export default RackConstructionComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js
new file mode 100644
index 00000000..e04287a5
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { startRackConstruction, stopRackConstruction } from '../../../../redux/actions/topology/room'
+import RackConstructionComponent from './RackConstructionComponent'
+
+function RackConstructionContainer(props) {
+ const isRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode)
+ const isEditingRoom = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
+
+ const dispatch = useDispatch()
+ const onStart = () => dispatch(startRackConstruction())
+ const onStop = () => dispatch(stopRackConstruction())
+ return (
+ <RackConstructionComponent
+ {...props}
+ inRackConstructionMode={isRackConstructionMode}
+ isEditingRoom={isEditingRoom}
+ onStart={onStart}
+ onStop={onStop}
+ />
+ )
+}
+
+export default RackConstructionContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomName.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomName.js
new file mode 100644
index 00000000..72d45bea
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomName.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import NameComponent from '../NameComponent'
+import { editRoomName } from '../../../../redux/actions/topology/room'
+
+function RoomName({ roomId }) {
+ const { name: roomName, id } = useSelector((state) => state.topology.rooms[roomId])
+ const dispatch = useDispatch()
+ const callback = (name) => {
+ if (name) {
+ dispatch(editRoomName(id, name))
+ }
+ }
+ return <NameComponent name={roomName} onEdit={callback} />
+}
+
+RoomName.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default RoomName
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomSidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomSidebar.js
new file mode 100644
index 00000000..6ad489e0
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomSidebar.js
@@ -0,0 +1,43 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import RoomName from './RoomName'
+import RackConstructionContainer from './RackConstructionContainer'
+import EditRoomContainer from './EditRoomContainer'
+import DeleteRoomContainer from './DeleteRoomContainer'
+import {
+ TextContent,
+ TextList,
+ TextListItem,
+ TextListItemVariants,
+ TextListVariants,
+ Title,
+} from '@patternfly/react-core'
+
+const RoomSidebar = ({ roomId }) => {
+ return (
+ <TextContent>
+ <Title headingLevel="h2">Details</Title>
+ <TextList component={TextListVariants.dl}>
+ <TextListItem
+ component={TextListItemVariants.dt}
+ className="pf-u-display-inline-flex pf-u-align-items-center"
+ >
+ Name
+ </TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>
+ <RoomName roomId={roomId} />
+ </TextListItem>
+ </TextList>
+ <Title headingLevel="h2">Construction</Title>
+ <RackConstructionContainer />
+ <EditRoomContainer roomId={roomId} />
+ <DeleteRoomContainer roomId={roomId} />
+ </TextContent>
+ )
+}
+
+RoomSidebar.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default RoomSidebar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/util/TableEmptyState.js b/opendc-web/opendc-web-server/src/main/webui/components/util/TableEmptyState.js
new file mode 100644
index 00000000..9d16ffbb
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/util/TableEmptyState.js
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { Bullseye, EmptyState, EmptyStateBody, EmptyStateIcon, Spinner, Title } from '@patternfly/react-core'
+import { SearchIcon, CubesIcon } from '@patternfly/react-icons'
+import { Status } from '../../shapes'
+
+function TableEmptyState({
+ status,
+ isFiltering,
+ loadingTitle = 'Loading',
+ emptyTitle = 'No results found',
+ emptyText = 'No results found of this type.',
+ emptyAction = '',
+}) {
+ if (status === 'loading') {
+ return (
+ <Bullseye>
+ <EmptyState variant="xs">
+ <EmptyStateIcon variant="container" component={Spinner} />
+ <Title headingLevel="h4" size="md">
+ {loadingTitle}
+ </Title>
+ </EmptyState>
+ </Bullseye>
+ )
+ } else if (status === 'error') {
+ return (
+ <EmptyState variant="xs">
+ <Title headingLevel="h4" size="md">
+ Unable to connect
+ </Title>
+ <EmptyStateBody>
+ There was an error retrieving data. Check your connection and try again.
+ </EmptyStateBody>
+ </EmptyState>
+ )
+ } else if (status === 'idle') {
+ return (
+ <EmptyState variant="xs">
+ <EmptyStateIcon icon={CubesIcon} />
+ <Title headingLevel="h4" size="md">
+ {emptyTitle}
+ </Title>
+ <EmptyStateBody>No results available at this moment.</EmptyStateBody>
+ </EmptyState>
+ )
+ } else if (isFiltering) {
+ return (
+ <EmptyState variant="xs">
+ <EmptyStateIcon icon={SearchIcon} />
+ <Title headingLevel="h4" size="md">
+ No results found
+ </Title>
+ <EmptyStateBody>
+ No results match this filter criteria. Remove all filters or clear all filters to show results.
+ </EmptyStateBody>
+ </EmptyState>
+ )
+ }
+
+ return (
+ <EmptyState variant="xs">
+ <EmptyStateIcon icon={CubesIcon} />
+ <Title headingLevel="h4" size="md">
+ {emptyTitle}
+ </Title>
+ <EmptyStateBody>{emptyText}</EmptyStateBody>
+ {emptyAction}
+ </EmptyState>
+ )
+}
+
+TableEmptyState.propTypes = {
+ status: Status.isRequired,
+ isFiltering: PropTypes.bool,
+ loadingTitle: PropTypes.string,
+ emptyTitle: PropTypes.string,
+ emptyText: PropTypes.string,
+ emptyAction: PropTypes.node,
+}
+
+export default TableEmptyState
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/util/modals/ConfirmationModal.js b/opendc-web/opendc-web-server/src/main/webui/components/util/modals/ConfirmationModal.js
new file mode 100644
index 00000000..f6e1c98b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/util/modals/ConfirmationModal.js
@@ -0,0 +1,27 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Modal from './Modal'
+
+function ConfirmationModal({ title, message, isOpen, callback }) {
+ return (
+ <Modal
+ title={title}
+ isOpen={isOpen}
+ onSubmit={() => callback(true)}
+ onCancel={() => callback(false)}
+ submitButtonType="danger"
+ submitButtonText="Confirm"
+ >
+ {message}
+ </Modal>
+ )
+}
+
+ConfirmationModal.propTypes = {
+ title: PropTypes.string.isRequired,
+ message: PropTypes.string.isRequired,
+ isOpen: PropTypes.bool.isRequired,
+ callback: PropTypes.func.isRequired,
+}
+
+export default ConfirmationModal
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/util/modals/Modal.js b/opendc-web/opendc-web-server/src/main/webui/components/util/modals/Modal.js
new file mode 100644
index 00000000..d4577062
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/util/modals/Modal.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Button, Modal as PModal, ModalVariant } from '@patternfly/react-core'
+
+function Modal({ children, title, isOpen, onSubmit, onCancel, submitButtonType, submitButtonText }) {
+ const actions = [
+ <Button variant={submitButtonType} onClick={onSubmit} key="confirm">
+ {submitButtonText}
+ </Button>,
+ <Button variant="link" onClick={onCancel} key="cancel">
+ Cancel
+ </Button>,
+ ]
+
+ return (
+ <PModal variant={ModalVariant.small} isOpen={isOpen} onClose={onCancel} title={title} actions={actions}>
+ {children}
+ </PModal>
+ )
+}
+
+Modal.propTypes = {
+ title: PropTypes.string.isRequired,
+ isOpen: PropTypes.bool,
+ onSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+ submitButtonType: PropTypes.string,
+ submitButtonText: PropTypes.string,
+ children: PropTypes.node,
+}
+
+Modal.defaultProps = {
+ submitButtonType: 'primary',
+ submitButtonText: 'Save',
+ isOpen: false,
+}
+
+export default Modal
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/util/modals/TextInputModal.js b/opendc-web/opendc-web-server/src/main/webui/components/util/modals/TextInputModal.js
new file mode 100644
index 00000000..392a729e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/util/modals/TextInputModal.js
@@ -0,0 +1,70 @@
+import PropTypes from 'prop-types'
+import React, { useRef, useState } from 'react'
+import Modal from './Modal'
+import { Form, FormGroup, TextInput } from '@patternfly/react-core'
+
+function TextInputModal({ title, label, isOpen, callback, initialValue }) {
+ const textInput = useRef(null)
+ const [isSubmitted, setSubmitted] = useState(false)
+ const [isValid, setValid] = useState(true)
+
+ const resetState = () => {
+ textInput.current.value = ''
+ setSubmitted(false)
+ setValid(false)
+ }
+ const onSubmit = (event) => {
+ const value = textInput.current.value
+ setSubmitted(true)
+
+ if (event) {
+ event.preventDefault()
+ }
+
+ if (!value) {
+ setValid(false)
+ return false
+ }
+
+ callback(value)
+ resetState()
+ return true
+ }
+ const onCancel = () => {
+ callback(undefined)
+ resetState()
+ }
+
+ return (
+ <Modal title={title} isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form onSubmit={onSubmit}>
+ <FormGroup
+ label={label}
+ fieldId="text-input"
+ isRequired
+ validated={isSubmitted && !isValid ? 'error' : 'default'}
+ helperTextInvalid="This field cannot be empty"
+ >
+ <TextInput
+ id="text-input"
+ name="text-input"
+ isRequired
+ type="text"
+ ref={textInput}
+ defaultValue={initialValue}
+ />
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+}
+
+TextInputModal.propTypes = {
+ title: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ isOpen: PropTypes.bool.isRequired,
+ callback: PropTypes.func.isRequired,
+ initialValue: PropTypes.string,
+}
+
+export default TextInputModal
diff --git a/opendc-web/opendc-web-server/src/main/webui/config.js b/opendc-web/opendc-web-server/src/main/webui/config.js
new file mode 100644
index 00000000..1a0ba02c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/config.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { env } from 'next-runtime-env';
+
+/**
+ * URL to OpenDC API.
+ */
+export const apiUrl = env("NEXT_PUBLIC_API_BASE_URL")
+
+/**
+ * Authentication configuration.
+ */
+export const auth = {
+ domain: env("NEXT_PUBLIC_AUTH0_DOMAIN"),
+ clientId: env("NEXT_PUBLIC_AUTH0_CLIENT_ID"),
+ audience: env("NEXT_PUBLIC_AUTH0_AUDIENCE"),
+ redirectUri: global.window && global.window.location.origin,
+}
+
+/**
+ * Sentry DSN for web frontend.
+ */
+export const sentryDsn = env("NEXT_PUBLIC_SENTRY_DSN")
diff --git a/opendc-web/opendc-web-server/src/main/webui/data/experiments.js b/opendc-web/opendc-web-server/src/main/webui/data/experiments.js
new file mode 100644
index 00000000..ca8912a2
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/data/experiments.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useQuery } from 'react-query'
+import { fetchTraces } from '../api/traces'
+import { fetchSchedulers } from '../api/schedulers'
+
+/**
+ * Configure the query defaults for the experiment endpoints.
+ */
+export function configureExperimentClient(queryClient, auth) {
+ queryClient.setQueryDefaults('traces', { queryFn: () => fetchTraces(auth) })
+ queryClient.setQueryDefaults('schedulers', { queryFn: () => fetchSchedulers(auth) })
+}
+
+/**
+ * Return the available traces to experiment with.
+ */
+export function useTraces(options) {
+ return useQuery('traces', options)
+}
+
+/**
+ * Return the available schedulers to experiment with.
+ */
+export function useSchedulers(options) {
+ return useQuery('schedulers', options)
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/data/project.js b/opendc-web/opendc-web-server/src/main/webui/data/project.js
new file mode 100644
index 00000000..60a8fab6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/data/project.js
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useQuery, useMutation } from 'react-query'
+import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects'
+import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfolios } from '../api/portfolios'
+import { addScenario, deleteScenario, fetchScenario } from '../api/scenarios'
+
+/**
+ * Configure the query defaults for the project endpoints.
+ */
+export function configureProjectClient(queryClient, auth) {
+ queryClient.setQueryDefaults('projects', {
+ queryFn: ({ queryKey }) => (queryKey.length === 1 ? fetchProjects(auth) : fetchProject(auth, queryKey[1])),
+ })
+
+ queryClient.setMutationDefaults('addProject', {
+ mutationFn: (data) => addProject(auth, data),
+ onSuccess: async (result) => {
+ queryClient.setQueryData('projects', (old = []) => [...old, result])
+ queryClient.setQueryData(['projects', result.id], result)
+ },
+ })
+ queryClient.setMutationDefaults('deleteProject', {
+ mutationFn: (id) => deleteProject(auth, id),
+ onSuccess: async (result) => {
+ queryClient.setQueryData('projects', (old = []) => old.filter((project) => project.id !== result.id))
+ queryClient.removeQueries(['projects', result.id])
+ },
+ })
+
+ queryClient.setQueryDefaults('portfolios', {
+ queryFn: ({ queryKey }) =>
+ queryKey.length === 2 ? fetchPortfolios(auth, queryKey[1]) : fetchPortfolio(auth, queryKey[1], queryKey[2]),
+ })
+ queryClient.setMutationDefaults('addPortfolio', {
+ mutationFn: ({ projectId, ...data }) => addPortfolio(auth, projectId, data),
+ onSuccess: async (result) => {
+ queryClient.setQueryData(['portfolios', result.project.id], (old = []) => [...old, result])
+ queryClient.setQueryData(['portfolios', result.project.id, result.number], result)
+ },
+ })
+ queryClient.setMutationDefaults('deletePortfolio', {
+ mutationFn: ({ projectId, number }) => deletePortfolio(auth, projectId, number),
+ onSuccess: async (result) => {
+ queryClient.setQueryData(['portfolios', result.project.id], (old = []) =>
+ old.filter((portfolio) => portfolio.id !== result.id)
+ )
+ queryClient.removeQueries(['portfolios', result.project.id, result.number])
+ },
+ })
+
+ queryClient.setQueryDefaults('scenarios', {
+ queryFn: ({ queryKey }) => fetchScenario(auth, queryKey[1], queryKey[2]),
+ })
+ queryClient.setMutationDefaults('addScenario', {
+ mutationFn: ({ projectId, portfolioNumber, data }) => addScenario(auth, projectId, portfolioNumber, data),
+ onSuccess: async (result) => {
+ // Register updated scenario in cache
+ queryClient.setQueryData(['scenarios', result.project.id, result.id], result)
+ queryClient.setQueryData(['portfolios', result.project.id, result.portfolio.number], (old) => ({
+ ...old,
+ scenarios: [...old.scenarios, result],
+ }))
+ },
+ })
+ queryClient.setMutationDefaults('deleteScenario', {
+ mutationFn: ({ projectId, number }) => deleteScenario(auth, projectId, number),
+ onSuccess: async (result) => {
+ queryClient.removeQueries(['scenarios', result.project.id, result.id])
+ queryClient.setQueryData(['portfolios', result.project.id, result.portfolio.number], (old) => ({
+ ...old,
+ scenarios: old?.scenarios?.filter((scenario) => scenario.id !== result.id),
+ }))
+ },
+ })
+}
+
+/**
+ * Return the available projects.
+ */
+export function useProjects(options = {}) {
+ return useQuery('projects', options)
+}
+
+/**
+ * Return the project with the specified identifier.
+ */
+export function useProject(projectId, options = {}) {
+ return useQuery(['projects', projectId], { enabled: !!projectId, ...options })
+}
+
+/**
+ * Create a mutation for a new project.
+ */
+export function useNewProject() {
+ return useMutation('addProject')
+}
+
+/**
+ * Create a mutation for deleting a project.
+ */
+export function useDeleteProject() {
+ return useMutation('deleteProject')
+}
+
+/**
+ * Return the portfolio with the specified identifier.
+ */
+export function usePortfolio(projectId, portfolioId, options = {}) {
+ return useQuery(['portfolios', projectId, portfolioId], { enabled: !!(projectId && portfolioId), ...options })
+}
+
+/**
+ * Return the portfolios of the specified project.
+ */
+export function usePortfolios(projectId, options = {}) {
+ return useQuery(['portfolios', projectId], { enabled: !!projectId, ...options })
+}
+
+/**
+ * Create a mutation for a new portfolio.
+ */
+export function useNewPortfolio() {
+ return useMutation('addPortfolio')
+}
+
+/**
+ * Create a mutation for deleting a portfolio.
+ */
+export function useDeletePortfolio() {
+ return useMutation('deletePortfolio')
+}
+
+/**
+ * Create a mutation for a new scenario.
+ */
+export function useNewScenario() {
+ return useMutation('addScenario')
+}
+
+/**
+ * Create a mutation for deleting a scenario.
+ */
+export function useDeleteScenario() {
+ return useMutation('deleteScenario')
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/data/query.js b/opendc-web/opendc-web-server/src/main/webui/data/query.js
new file mode 100644
index 00000000..3e5423b9
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/data/query.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useMemo } from 'react'
+import { QueryClient } from 'react-query'
+import { useAuth } from '../auth'
+import { configureExperimentClient } from './experiments'
+import { configureProjectClient } from './project'
+import { configureTopologyClient } from './topology'
+import { configureUserClient } from './user'
+
+let queryClient
+
+function createQueryClient(auth) {
+ const client = new QueryClient()
+ configureProjectClient(client, auth)
+ configureExperimentClient(client, auth)
+ configureTopologyClient(client, auth)
+ configureUserClient(client, auth)
+ return client
+}
+
+function initializeQueryClient(auth) {
+ const _queryClient = queryClient ?? createQueryClient(auth)
+
+ // For SSG and SSR always create a new query client
+ if (typeof window === 'undefined') return _queryClient
+ // Create the query client once in the client
+ if (!queryClient) queryClient = _queryClient
+
+ return _queryClient
+}
+
+/**
+ * Obtain a cached query client.
+ */
+export function useNewQueryClient() {
+ const auth = useAuth()
+ return useMemo(() => initializeQueryClient(auth), []) // eslint-disable-line react-hooks/exhaustive-deps
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/data/topology.js b/opendc-web/opendc-web-server/src/main/webui/data/topology.js
new file mode 100644
index 00000000..d5e624d5
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/data/topology.js
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useQuery, useMutation } from 'react-query'
+import { addTopology, deleteTopology, fetchTopologies, fetchTopology, updateTopology } from '../api/topologies'
+
+/**
+ * Configure the query defaults for the topology endpoints.
+ */
+export function configureTopologyClient(queryClient, auth) {
+ queryClient.setQueryDefaults('topologies', {
+ queryFn: ({ queryKey }) =>
+ queryKey.length === 2 ? fetchTopologies(auth, queryKey[1]) : fetchTopology(auth, queryKey[1], queryKey[2]),
+ })
+
+ queryClient.setMutationDefaults('addTopology', {
+ mutationFn: ({ projectId, ...data }) => addTopology(auth, projectId, data),
+ onSuccess: (result) => {
+ queryClient.setQueryData(['topologies', result.project.id], (old = []) => [...old, result])
+ queryClient.setQueryData(['topologies', result.project.id, result.number], result)
+ },
+ })
+ queryClient.setMutationDefaults('updateTopology', {
+ mutationFn: (data) => updateTopology(auth, data),
+ onSuccess: (result) => {
+ queryClient.setQueryData(['topologies', result.project.id], (old = []) =>
+ old.map((topology) => (topology.id === result.id ? result : topology))
+ )
+ queryClient.setQueryData(['topologies', result.project.id, result.number], result)
+ },
+ })
+ queryClient.setMutationDefaults('deleteTopology', {
+ mutationFn: ({ projectId, number }) => deleteTopology(auth, projectId, number),
+ onSuccess: (result) => {
+ queryClient.setQueryData(['topologies', result.project.id], (old = []) =>
+ old.filter((topology) => topology.id !== result.id)
+ )
+ queryClient.removeQueries(['topologies', result.project.id, result.number])
+ },
+ })
+}
+
+/**
+ * Fetch the topology with the specified identifier for the specified project.
+ */
+export function useTopology(projectId, topologyId, options = {}) {
+ return useQuery(['topologies', projectId, topologyId], { enabled: !!(projectId && topologyId), ...options })
+}
+
+/**
+ * Fetch all topologies of the specified project.
+ */
+export function useTopologies(projectId, options = {}) {
+ return useQuery(['topologies', projectId], { enabled: !!projectId, ...options })
+}
+
+/**
+ * Create a mutation for a new topology.
+ */
+export function useNewTopology() {
+ return useMutation('addTopology')
+}
+
+/**
+ * Create a mutation for deleting a topology.
+ */
+export function useDeleteTopology(options = {}) {
+ return useMutation('deleteTopology', options)
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/data/user.js b/opendc-web/opendc-web-server/src/main/webui/data/user.js
new file mode 100644
index 00000000..97c0e1e2
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/data/user.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useQuery } from 'react-query'
+import { fetchUser } from '../api/users'
+
+/**
+ * Configure the query defaults for the user client.
+ */
+export function configureUserClient(queryClient, auth) {
+ queryClient.setQueryDefaults('user', {
+ queryFn: () => fetchUser(auth),
+ })
+}
+
+/**
+ * Fetch the user data on the server.
+ */
+export default function useUser(options = {}) {
+ return useQuery('user', options)
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/next.config.js b/opendc-web/opendc-web-server/src/main/webui/next.config.js
new file mode 100644
index 00000000..a11b4778
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/next.config.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+// PatternFly 4 uses global CSS imports in its distribution files. Therefore,
+// we need to transpile the modules before we can use them.
+const { withGlobalCss } = require('next-global-css')
+const withConfig = withGlobalCss()
+
+// Generate dynamic env file
+const { configureRuntimeEnv } = require('next-runtime-env/build/configure');
+
+configureRuntimeEnv();
+
+module.exports = (phase) => withConfig({
+ basePath: process.env.NEXT_BASE_PATH && '/' + process.env.NEXT_BASE_PATH,
+ reactStrictMode: true,
+ async redirects() {
+ return [
+ {
+ source: '/',
+ destination: '/projects',
+ permanent: false
+ }
+ ]
+ },
+ images: { unoptimized: true }
+})
diff --git a/opendc-web/opendc-web-server/src/main/webui/package-lock.json b/opendc-web/opendc-web-server/src/main/webui/package-lock.json
new file mode 100644
index 00000000..71add29b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/package-lock.json
@@ -0,0 +1,8146 @@
+{
+ "name": "opendc-frontend",
+ "version": "3.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "opendc-frontend",
+ "version": "3.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@auth0/auth0-react": "^1.12.1",
+ "@patternfly/react-charts": "^6.94.18",
+ "@patternfly/react-core": "^4.276.6",
+ "@patternfly/react-icons": "^4.93.6",
+ "@patternfly/react-table": "^4.112.39",
+ "@sentry/react": "^7.45.0",
+ "@sentry/tracing": "^7.45.0",
+ "clsx": "^1.2.1",
+ "immer": "^9.0.21",
+ "konva": "^8.4.3",
+ "mathjs": "^11.7.0",
+ "next": "^13.5.4",
+ "next-global-css": "^1.3.1",
+ "next-runtime-env": "^1.7.1",
+ "normalizr": "^3.6.2",
+ "prettier": "^2.8.7",
+ "prop-types": "^15.8.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-hotkeys-hook": "^4.3.8",
+ "react-konva": "^18.2.5",
+ "react-query": "^3.39.3",
+ "react-redux": "^8.0.5",
+ "redux": "^4.2.1",
+ "redux-logger": "^3.0.6",
+ "redux-saga": "^1.2.3",
+ "redux-thunk": "^2.4.2",
+ "svgsaver": "^0.9.0",
+ "use-resize-observer": "^9.1.0",
+ "uuid": "^9.0.0",
+ "victory-errorbar": "^36.6.8"
+ },
+ "devDependencies": {
+ "eslint": "^8.36.0",
+ "eslint-config-next": "^13.2.4"
+ }
+ },
+ "node_modules/@auth0/auth0-react": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-1.12.1.tgz",
+ "integrity": "sha512-8+ecK/4rE0AGsxLW2IDcr1oPbT55tuE6cQEzEIOkQjB6QGQxxWMzQy0D4nMKw3JUAc7nYcFVOABNFNbc471n9Q==",
+ "dependencies": {
+ "@auth0/auth0-spa-js": "^1.22.6"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17 || ^18",
+ "react-dom": "^16.11.0 || ^17 || ^18"
+ }
+ },
+ "node_modules/@auth0/auth0-spa-js": {
+ "version": "1.22.6",
+ "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-1.22.6.tgz",
+ "integrity": "sha512-iL3O0vWanfKFVgy1J2ZHDPlAUK6EVHWEHWS6mUXwHEuPiK39tjlQtyUKQIJI1F5YsZB75ijGgRWMTawSDXlwCA==",
+ "dependencies": {
+ "abortcontroller-polyfill": "^1.7.3",
+ "browser-tabs-lock": "^1.2.15",
+ "core-js": "^3.25.4",
+ "es-cookie": "~1.3.2",
+ "fast-text-encoding": "^1.0.6",
+ "promise-polyfill": "^8.2.3",
+ "unfetch": "^4.2.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
+ "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/runtime-corejs3": {
+ "version": "7.19.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.6.tgz",
+ "integrity": "sha512-oWNn1ZlGde7b4i/3tnixpH9qI0bOAACiUs+KEES4UUCnsPjVWFlWdLV/iwJuPC2qp3EowbAqsm+0XqNwnwYhxA==",
+ "dev": true,
+ "dependencies": {
+ "core-js-pure": "^3.25.1",
+ "regenerator-runtime": "^0.13.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "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.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.1.tgz",
+ "integrity": "sha512-BISJ6ZE4xQsuL/FmsyRaiffpq977bMlsKfGHTQrOGFErfByxIe6iZTxPf/00Zon9b9a7iUykfQwejN3s2ZW/Bw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz",
+ "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.5.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz",
+ "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+ "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "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==",
+ "dev": true
+ },
+ "node_modules/@juggle/resize-observer": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
+ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
+ },
+ "node_modules/@next/env": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz",
+ "integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ=="
+ },
+ "node_modules/@next/eslint-plugin-next": {
+ "version": "13.2.4",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.4.tgz",
+ "integrity": "sha512-ck1lI+7r1mMJpqLNa3LJ5pxCfOB1lfJncKmRJeJxcJqcngaFwylreLP7da6Rrjr6u2gVRTfmnkSkjc80IiQCwQ==",
+ "dev": true,
+ "dependencies": {
+ "glob": "7.1.7"
+ }
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz",
+ "integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz",
+ "integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz",
+ "integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz",
+ "integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz",
+ "integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz",
+ "integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz",
+ "integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz",
+ "integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz",
+ "integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@patternfly/react-charts": {
+ "version": "6.94.18",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-charts/-/react-charts-6.94.18.tgz",
+ "integrity": "sha512-56WxnZYC3blRX41mW67JaPxJ3YhXViLvwGpEsZrYCccla/rTV8JgKK0gjHnqtzPQiVBfpn+3ewOyNCOR5uRoSw==",
+ "dependencies": {
+ "@patternfly/react-styles": "^4.92.6",
+ "@patternfly/react-tokens": "^4.94.6",
+ "hoist-non-react-statics": "^3.3.0",
+ "lodash": "^4.17.19",
+ "tslib": "^2.0.0",
+ "victory-area": "^36.6.7",
+ "victory-axis": "^36.6.7",
+ "victory-bar": "^36.6.7",
+ "victory-chart": "^36.6.7",
+ "victory-core": "^36.6.7",
+ "victory-create-container": "^36.6.7",
+ "victory-cursor-container": "^36.6.7",
+ "victory-group": "^36.6.7",
+ "victory-legend": "^36.6.7",
+ "victory-line": "^36.6.7",
+ "victory-pie": "^36.6.7",
+ "victory-scatter": "^36.6.7",
+ "victory-stack": "^36.6.7",
+ "victory-tooltip": "^36.6.7",
+ "victory-voronoi-container": "^36.6.7",
+ "victory-zoom-container": "^36.6.7"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18",
+ "react-dom": "^16.8 || ^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-core": {
+ "version": "4.276.6",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.276.6.tgz",
+ "integrity": "sha512-G0K+378jf9jw9g+hCAoKnsAe/UGTRspqPeuAYypF2FgNr+dC7dUpc7/VkNhZBVqSJzUWVEK8NyXcqkfi0IemIg==",
+ "dependencies": {
+ "@patternfly/react-icons": "^4.93.6",
+ "@patternfly/react-styles": "^4.92.6",
+ "@patternfly/react-tokens": "^4.94.6",
+ "focus-trap": "6.9.2",
+ "react-dropzone": "9.0.0",
+ "tippy.js": "5.1.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18",
+ "react-dom": "^16.8 || ^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-icons": {
+ "version": "4.93.6",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.93.6.tgz",
+ "integrity": "sha512-ZrXegc/81oiuTIeWvoHb3nG/eZODbB4rYmekBEsrbiysyO7m/sUFoi/RLvgFINrRoh6YCJqL5fj06Jg6d7jX1g==",
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18",
+ "react-dom": "^16.8 || ^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-styles": {
+ "version": "4.92.6",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.92.6.tgz",
+ "integrity": "sha512-b8uQdEReMyeoMzjpMri845QxqtupY/tIFFYfVeKoB2neno8gkcW1RvDdDe62LF88q45OktCwAe/8A99ker10Iw=="
+ },
+ "node_modules/@patternfly/react-table": {
+ "version": "4.112.39",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.112.39.tgz",
+ "integrity": "sha512-U+hOMgYlbghGH4M5MX+qt0GkVi/ocrGnxDnm11YiS3CtEGsd6Rr0NeqMmk0uoR46Od4Pr5tKuXxZhPP32sCL/w==",
+ "dependencies": {
+ "@patternfly/react-core": "^4.276.6",
+ "@patternfly/react-icons": "^4.93.6",
+ "@patternfly/react-styles": "^4.92.6",
+ "@patternfly/react-tokens": "^4.94.6",
+ "lodash": "^4.17.19",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18",
+ "react-dom": "^16.8 || ^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-tokens": {
+ "version": "4.94.6",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.94.6.tgz",
+ "integrity": "sha512-tm7C6nat+uKMr1hrapis7hS3rN9cr41tpcCKhx6cod6FLU8KwF2Yt5KUxakhIOCEcE/M/EhXhAw/qejp8w0r7Q=="
+ },
+ "node_modules/@pkgr/utils": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
+ "integrity": "sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "is-glob": "^4.0.3",
+ "open": "^8.4.0",
+ "picocolors": "^1.0.0",
+ "tiny-glob": "^0.2.9",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@redux-saga/core": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.3.tgz",
+ "integrity": "sha512-U1JO6ncFBAklFTwoQ3mjAeQZ6QGutsJzwNBjgVLSWDpZTRhobUzuVDS1qH3SKGJD8fvqoaYOjp6XJ3gCmeZWgA==",
+ "dependencies": {
+ "@babel/runtime": "^7.6.3",
+ "@redux-saga/deferred": "^1.2.1",
+ "@redux-saga/delay-p": "^1.2.1",
+ "@redux-saga/is": "^1.1.3",
+ "@redux-saga/symbols": "^1.1.3",
+ "@redux-saga/types": "^1.2.1",
+ "redux": "^4.0.4",
+ "typescript-tuple": "^2.2.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/redux-saga"
+ }
+ },
+ "node_modules/@redux-saga/deferred": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz",
+ "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g=="
+ },
+ "node_modules/@redux-saga/delay-p": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz",
+ "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==",
+ "dependencies": {
+ "@redux-saga/symbols": "^1.1.3"
+ }
+ },
+ "node_modules/@redux-saga/is": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz",
+ "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==",
+ "dependencies": {
+ "@redux-saga/symbols": "^1.1.3",
+ "@redux-saga/types": "^1.2.1"
+ }
+ },
+ "node_modules/@redux-saga/symbols": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz",
+ "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg=="
+ },
+ "node_modules/@redux-saga/types": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz",
+ "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA=="
+ },
+ "node_modules/@rushstack/eslint-patch": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
+ "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==",
+ "dev": true
+ },
+ "node_modules/@sentry-internal/tracing": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.45.0.tgz",
+ "integrity": "sha512-0aIDY2OvUX7k2XHaimOlWkboXoQvJ9dEKvfpu0Wh0YxfUTGPa+wplUdg3WVdkk018sq1L11MKmj4MPZyYUvXhw==",
+ "dependencies": {
+ "@sentry/core": "7.45.0",
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry-internal/tracing/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@sentry/browser": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.45.0.tgz",
+ "integrity": "sha512-/dUrUwnI34voMj+jSJT7b5Jun+xy1utVyzzwTq3Oc22N+SB17ZOX9svZ4jl1Lu6tVJPVjPyvL6zlcbrbMwqFjg==",
+ "dependencies": {
+ "@sentry-internal/tracing": "7.45.0",
+ "@sentry/core": "7.45.0",
+ "@sentry/replay": "7.45.0",
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/browser/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@sentry/core": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.45.0.tgz",
+ "integrity": "sha512-xJfdTS4lRmHvZI/A5MazdnKhBJFkisKu6G9EGNLlZLre+6W4PH5sb7QX4+xoBdqG7v10Jvdia112vi762ojO2w==",
+ "dependencies": {
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/core/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@sentry/react": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.45.0.tgz",
+ "integrity": "sha512-Dbz85nfvMUikbLHUuIt6fBNPmTvThFn+rWB5KS1NIOJifyWAdpIU3X7yCUJE5xhsUObNLiHlNJlqhaQI4nR1bQ==",
+ "dependencies": {
+ "@sentry/browser": "7.45.0",
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0",
+ "hoist-non-react-statics": "^3.3.2",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "react": "15.x || 16.x || 17.x || 18.x"
+ }
+ },
+ "node_modules/@sentry/react/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@sentry/replay": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.45.0.tgz",
+ "integrity": "sha512-smM7FIcFIyKu30BqCl8BzLo1gH/z9WwXdGX6V0fNvHab9fJZ09+xjFn+LmIyo6N8H8jjwsup0+yQ12kiF/ZsEw==",
+ "dependencies": {
+ "@sentry/core": "7.45.0",
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sentry/tracing": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.45.0.tgz",
+ "integrity": "sha512-FsoFmZPzTBGvWeJH73NxSF1ot61Zw3aIZo5XolengiKnRmcrQOFxebtMKBiZ61QBRYGqsm5uT7QB7zITU6Ikgg==",
+ "dependencies": {
+ "@sentry-internal/tracing": "7.45.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/types": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.45.0.tgz",
+ "integrity": "sha512-iFt7msfUK8LCodFF3RKUyaxy9tJv/gpWhzxUFyNxtuVwlpmd+q6mtsFGn8Af3pbpm8A+MKyz1ebMwXj0PQqknw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/utils": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.45.0.tgz",
+ "integrity": "sha512-aTY7qqtNUudd09SH5DVSKMm3iQ6ZeWufduc0I9bPZe6UMM09BDc4KmjmrzRkdQ+VaOmHo7+v+HZKQk5f+AbuTQ==",
+ "dependencies": {
+ "@sentry/types": "7.45.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/utils/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
+ "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz",
+ "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ=="
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA=="
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz",
+ "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA=="
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz",
+ "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg=="
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.0.tgz",
+ "integrity": "sha512-jYIYxFFA9vrJ8Hd4Se83YI6XF+gzDL1aC5DCsldai4XYYiVNdhtpGbA/GM6iyQ8ayhSp3a148LY34hy7A4TxZA==",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
+ "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg=="
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz",
+ "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g=="
+ },
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+ },
+ "node_modules/@types/react": {
+ "version": "18.0.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.23.tgz",
+ "integrity": "sha512-R1wTULtCiJkudAN2DJGoYYySbGtOdzZyUWAACYinKdiQC8auxso4kLDUhQ7AJ2kh3F6A6z4v69U6tNY39hihVQ==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-reconciler": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.2.tgz",
+ "integrity": "sha512-8tu6lHzEgYPlfDf/J6GOQdIc+gs+S2yAqlby3zTsB3SP2svlqTYe5fwZNtZyfactP74ShooP2vvi1BOp9ZemWw==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.1.tgz",
+ "integrity": "sha512-kAV+NiNBWVQDY9gDJDToTE/NO8BHi4f6b7zTsVAJoTkmB/zlfOpiEVBzHOKtlgTndCKe8vj9F/PuolemZSh50Q==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.42.1",
+ "@typescript-eslint/types": "5.42.1",
+ "@typescript-eslint/typescript-estree": "5.42.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.1.tgz",
+ "integrity": "sha512-QAZY/CBP1Emx4rzxurgqj3rUinfsh/6mvuKbLNMfJMMKYLRBfweus8brgXF8f64ABkIZ3zdj2/rYYtF8eiuksQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.42.1",
+ "@typescript-eslint/visitor-keys": "5.42.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.1.tgz",
+ "integrity": "sha512-Qrco9dsFF5lhalz+lLFtxs3ui1/YfC6NdXu+RAGBa8uSfn01cjO7ssCsjIsUs484vny9Xm699FSKwpkCcqwWwA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.1.tgz",
+ "integrity": "sha512-qElc0bDOuO0B8wDhhW4mYVgi/LZL+igPwXtV87n69/kYC/7NG3MES0jHxJNCr4EP7kY1XVsRy8C/u3DYeTKQmw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.42.1",
+ "@typescript-eslint/visitor-keys": "5.42.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.1.tgz",
+ "integrity": "sha512-LOQtSF4z+hejmpUvitPlc4hA7ERGoj2BVkesOcG91HCn8edLGUXbTrErmutmPbl8Bo9HjAvOO/zBKQHExXNA2A==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.42.1",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/abortcontroller-polyfill": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz",
+ "integrity": "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ=="
+ },
+ "node_modules/acorn": {
+ "version": "8.8.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+ "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "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/aria-query": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
+ "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.10.2",
+ "@babel/runtime-corejs3": "^7.10.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+ "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+ "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
+ "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
+ "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==",
+ "dev": true
+ },
+ "node_modules/attr-accept": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz",
+ "integrity": "sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==",
+ "dependencies": {
+ "core-js": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/attr-accept/node_modules/core-js": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
+ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
+ "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
+ "hasInstallScript": true
+ },
+ "node_modules/axe-core": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.0.tgz",
+ "integrity": "sha512-4+rr8eQ7+XXS5nZrKcMO/AikHL0hVqy+lHWAnE3xdHl+aguag8SOQ6eEqLexwLNWgXIMfunGuD3ON1/6Kyet0A==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
+ "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==",
+ "dev": true
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/broadcast-channel": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
+ "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
+ "dependencies": {
+ "@babel/runtime": "^7.7.2",
+ "detect-node": "^2.1.0",
+ "js-sha3": "0.8.0",
+ "microseconds": "0.2.0",
+ "nano-time": "1.0.0",
+ "oblivious-set": "1.0.0",
+ "rimraf": "3.0.2",
+ "unload": "2.2.0"
+ }
+ },
+ "node_modules/browser-tabs-lock": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/browser-tabs-lock/-/browser-tabs-lock-1.2.15.tgz",
+ "integrity": "sha512-J8K9vdivK0Di+b8SBdE7EZxDr88TnATing7XoLw6+nFkXMQ6sVBh92K3NQvZlZU91AIkFRi0w3sztk5Z+vsswA==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "lodash": ">=4.17.21"
+ }
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001426",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001426.tgz",
+ "integrity": "sha512-n7cosrHLl8AWt0wwZw/PJZgUg3lV0gk9LMI7ikGJwhyhgsd2Nb65vKvmSexCqq/J7rbH3mFG6yZZiPR5dLPW5A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ }
+ ]
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
+ },
+ "node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "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=="
+ },
+ "node_modules/complex.js": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz",
+ "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://www.patreon.com/infusion"
+ }
+ },
+ "node_modules/computed-styles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/computed-styles/-/computed-styles-1.1.2.tgz",
+ "integrity": "sha512-CGbti1B791SKg6goVX0cSI++hFBSzY9+7+lhX8lqXDI5FHexluglI1cPtvIifS4mEcWxPJ+HKYPr2t6nqz7PxA=="
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "node_modules/core-js": {
+ "version": "3.29.1",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz",
+ "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==",
+ "hasInstallScript": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-js-pure": {
+ "version": "3.26.0",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.0.tgz",
+ "integrity": "sha512-LiN6fylpVBVwT8twhhluD9TzXmZQQsr2I2eIKtWNbZI1XMfBT7CV18itaN6RA7EtQd/SDdRx/wzvAShX2HvhQA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
+ "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz",
+ "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz",
+ "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz",
+ "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz",
+ "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
+ },
+ "node_modules/deep-diff": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
+ "integrity": "sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug=="
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
+ "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
+ "dev": true,
+ "dependencies": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delaunator": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
+ "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag=="
+ },
+ "node_modules/delaunay-find": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/delaunay-find/-/delaunay-find-0.0.6.tgz",
+ "integrity": "sha512-1+almjfrnR7ZamBk0q3Nhg6lqSe6Le4vL0WJDSMx4IDbQwTpUTXPjxC00lqLBT8MYsJpPCbI16sIkw9cPsbi7Q==",
+ "dependencies": {
+ "delaunator": "^4.0.0"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.12.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
+ "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz",
+ "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.1.3",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.2",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trimend": "^1.0.5",
+ "string.prototype.trimstart": "^1.0.5",
+ "unbox-primitive": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-cookie": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz",
+ "integrity": "sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q=="
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+ "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escape-latex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
+ "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz",
+ "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.4.0",
+ "@eslint/eslintrc": "^2.0.1",
+ "@eslint/js": "8.36.0",
+ "@humanwhocodes/config-array": "^0.11.8",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.1",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.5.0",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "grapheme-splitter": "^1.0.4",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-sdsl": "^4.1.4",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-next": {
+ "version": "13.2.4",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.2.4.tgz",
+ "integrity": "sha512-lunIBhsoeqw6/Lfkd6zPt25w1bn0znLA/JCL+au1HoEpSb4/PpsOYsYtgV/q+YPsoKIOzFyU5xnb04iZnXjUvg==",
+ "dev": true,
+ "dependencies": {
+ "@next/eslint-plugin-next": "13.2.4",
+ "@rushstack/eslint-patch": "^1.1.3",
+ "@typescript-eslint/parser": "^5.42.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-import-resolver-typescript": "^3.5.2",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
+ "eslint-plugin-react": "^7.31.7",
+ "eslint-plugin-react-hooks": "^4.5.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.23.0 || ^8.0.0",
+ "typescript": ">=3.3.1"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
+ "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.11.0",
+ "resolve": "^1.22.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.3.tgz",
+ "integrity": "sha512-njRcKYBc3isE42LaTcJNVANR3R99H9bAxBDMNDr2W7yq5gYPxbU3MkdhsQukxZ/Xg9C2vcyLlDsbKfRDg0QvCQ==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.3.4",
+ "enhanced-resolve": "^5.10.0",
+ "get-tsconfig": "^4.2.0",
+ "globby": "^13.1.2",
+ "is-core-module": "^2.10.0",
+ "is-glob": "^4.0.3",
+ "synckit": "^0.8.4"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript/node_modules/globby": {
+ "version": "13.1.3",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz",
+ "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==",
+ "dev": true,
+ "dependencies": {
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.11",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript/node_modules/slash": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+ "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz",
+ "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.27.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz",
+ "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "array.prototype.flatmap": "^1.3.1",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.7",
+ "eslint-module-utils": "^2.7.4",
+ "has": "^1.0.3",
+ "is-core-module": "^2.11.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.6",
+ "resolve": "^1.22.1",
+ "semver": "^6.3.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.6.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz",
+ "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.18.9",
+ "aria-query": "^4.2.2",
+ "array-includes": "^3.1.5",
+ "ast-types-flow": "^0.0.7",
+ "axe-core": "^4.4.3",
+ "axobject-query": "^2.2.0",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "has": "^1.0.3",
+ "jsx-ast-utils": "^3.3.2",
+ "language-tags": "^1.0.5",
+ "minimatch": "^3.1.2",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.31.10",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz",
+ "integrity": "sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.5",
+ "array.prototype.flatmap": "^1.3.0",
+ "doctrine": "^2.1.0",
+ "estraverse": "^5.3.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.5",
+ "object.fromentries": "^2.0.5",
+ "object.hasown": "^1.1.1",
+ "object.values": "^1.1.5",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.3",
+ "semver": "^6.3.0",
+ "string.prototype.matchall": "^4.0.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
+ "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.4",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
+ "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+ "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz",
+ "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.8.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "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"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "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.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fast-text-encoding": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz",
+ "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="
+ },
+ "node_modules/fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/file-saver": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz",
+ "integrity": "sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg=="
+ },
+ "node_modules/file-selector": {
+ "version": "0.1.19",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz",
+ "integrity": "sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==",
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=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==",
+ "dev": true
+ },
+ "node_modules/focus-trap": {
+ "version": "6.9.2",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.2.tgz",
+ "integrity": "sha512-gBEuXOPNOKPrLdZpMFUSTyIo1eT2NSZRrwZ9r/0Jqw5tmT3Yvxfmu8KBHw8xW2XQkw6E/JoG+OlEq7UDtSUNgw==",
+ "dependencies": {
+ "tabbable": "^5.3.2"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
+ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://www.patreon.com/infusion"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+ "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
+ "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.4.0.tgz",
+ "integrity": "sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "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.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
+ },
+ "node_modules/globals": {
+ "version": "13.20.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+ "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalyzer": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
+ "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==",
+ "dev": true
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
+ "dev": true
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "node_modules/grapheme-splitter": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+ "dev": true
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+ "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immer": {
+ "version": "9.0.21",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
+ "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+ "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "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-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/its-fine": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.0.6.tgz",
+ "integrity": "sha512-VZJZPwVT2kxe5KQv+TxCjojfLiUIut8zXDNLTxcM7gJ/xQ/bSPk5M0neZ+j3myy45KKkltY1mm1jyJgx3Fxsdg==",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": ">=18.0"
+ }
+ },
+ "node_modules/javascript-natural-sort": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
+ },
+ "node_modules/js-sdsl": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
+ "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==",
+ "dev": true
+ },
+ "node_modules/js-sha3": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "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": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
+ "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.5",
+ "object.assign": "^4.1.3"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/konva": {
+ "version": "8.4.3",
+ "resolved": "https://registry.npmjs.org/konva/-/konva-8.4.3.tgz",
+ "integrity": "sha512-ARqdgAbdNIougRlOKvkQwHlGhXPRBV4KvhCP+qoPpGoVQwwiJe4Hkdu4HHdRPb9rGUp04jDTAxBzEwBsE272pg==",
+ "funding": [
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/lavrton"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/konva"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/lavrton"
+ }
+ ]
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.22",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+ "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "dev": true
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz",
+ "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==",
+ "dev": true,
+ "dependencies": {
+ "language-subtag-registry": "~0.3.2"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "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/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/match-sorter": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz",
+ "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "remove-accents": "0.4.2"
+ }
+ },
+ "node_modules/mathjs": {
+ "version": "11.7.0",
+ "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.7.0.tgz",
+ "integrity": "sha512-RCXtrP5xGIbl9PUc5+7QL81rBCUjzoIZ0ugNqKsarOUxg+x7deY0BzfNai+bGfUL/T+1uYq1xs5w2xVdL3lp0g==",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0",
+ "complex.js": "^2.1.1",
+ "decimal.js": "^10.4.3",
+ "escape-latex": "^1.2.0",
+ "fraction.js": "^4.2.0",
+ "javascript-natural-sort": "^0.7.1",
+ "seedrandom": "^3.0.5",
+ "tiny-emitter": "^2.1.0",
+ "typed-function": "^4.1.0"
+ },
+ "bin": {
+ "mathjs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/microseconds": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
+ "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/nano-time": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
+ "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
+ "dependencies": {
+ "big-integer": "^1.6.16"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/next": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz",
+ "integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==",
+ "dependencies": {
+ "@next/env": "13.5.4",
+ "@swc/helpers": "0.5.2",
+ "busboy": "1.6.0",
+ "caniuse-lite": "^1.0.30001406",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.1",
+ "watchpack": "2.4.0"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=16.14.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "13.5.4",
+ "@next/swc-darwin-x64": "13.5.4",
+ "@next/swc-linux-arm64-gnu": "13.5.4",
+ "@next/swc-linux-arm64-musl": "13.5.4",
+ "@next/swc-linux-x64-gnu": "13.5.4",
+ "@next/swc-linux-x64-musl": "13.5.4",
+ "@next/swc-win32-arm64-msvc": "13.5.4",
+ "@next/swc-win32-ia32-msvc": "13.5.4",
+ "@next/swc-win32-x64-msvc": "13.5.4"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/next-global-css": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/next-global-css/-/next-global-css-1.3.1.tgz",
+ "integrity": "sha512-+OnTwQKmv1lDP7r4R3T94oq6372R9UGVivchBQu49j7ZjzvSXHCnv93yAuhgMkvUgAbGifTs8sQ5YL9wjyAxfA=="
+ },
+ "node_modules/next-runtime-env": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/next-runtime-env/-/next-runtime-env-1.7.1.tgz",
+ "integrity": "sha512-KIWciYVcYBoc0dBTyx7tEogyESacAJCkseU9+AcIYbkq3MiV6WyhHHPfDONpXbJSlpT3u3utrSrngua2EFlfyA==",
+ "dependencies": {
+ "chalk": "^4.1.2"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalizr": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/normalizr/-/normalizr-3.6.2.tgz",
+ "integrity": "sha512-30qCybsBaCBciotorvuOZTCGEg2AXrJfADMT2Kk/lvpIAcipHdK0zc33nNtwKzyfQAqIJXAcqET6YgflYUgsoQ=="
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
+ "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+ "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz",
+ "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz",
+ "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.hasown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz",
+ "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.19.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+ "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/oblivious-set": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
+ "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw=="
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "dev": true,
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "dependencies": {
+ "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"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "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/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/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/popper.js": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
+ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
+ "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
+ "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/promise-polyfill": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
+ "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg=="
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types-extra": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
+ "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
+ "dependencies": {
+ "react-is": "^16.3.2",
+ "warning": "^4.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=0.14.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/react-dropzone": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-9.0.0.tgz",
+ "integrity": "sha512-wZ2o9B2qkdE3RumWhfyZT9swgJYJPeU5qHEcMU8weYpmLex1eeWX0CC32/Y0VutB+BBi2D+iePV/YZIiB4kZGw==",
+ "dependencies": {
+ "attr-accept": "^1.1.3",
+ "file-selector": "^0.1.8",
+ "prop-types": "^15.6.2",
+ "prop-types-extra": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "react": ">=0.14.0"
+ }
+ },
+ "node_modules/react-fast-compare": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
+ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
+ },
+ "node_modules/react-hotkeys-hook": {
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.8.tgz",
+ "integrity": "sha512-RmrIQ3M259c84MnYVEAQsmHkD6s7XUgLG0rW6S7qjt1Lh7q+SPIz5b6obVU8OJw1Utsj1mUCj6twtBPaK/ytww==",
+ "peerDependencies": {
+ "react": ">=16.8.1",
+ "react-dom": ">=16.8.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "node_modules/react-konva": {
+ "version": "18.2.5",
+ "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.5.tgz",
+ "integrity": "sha512-lTqJStcHnpGSXB9RlV7p5at3MpRML/TujzbuUDZRIInsLocJ/I4Nhhg3w6yJm9UV05kcwr88OY6LO+2zRyzXog==",
+ "funding": [
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/lavrton"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/konva"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/lavrton"
+ }
+ ],
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.2",
+ "its-fine": "^1.0.6",
+ "react-reconciler": "~0.29.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "konva": "^8.0.1 || ^7.2.5",
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
+ }
+ },
+ "node_modules/react-query": {
+ "version": "3.39.3",
+ "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
+ "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "broadcast-channel": "^3.4.1",
+ "match-sorter": "^6.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-reconciler": {
+ "version": "0.29.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz",
+ "integrity": "sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/react-redux": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz",
+ "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.1",
+ "@types/hoist-non-react-statics": "^3.3.1",
+ "@types/use-sync-external-store": "^0.0.3",
+ "hoist-non-react-statics": "^3.3.2",
+ "react-is": "^18.0.0",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8 || ^17.0 || ^18.0",
+ "@types/react-dom": "^16.8 || ^17.0 || ^18.0",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0",
+ "react-native": ">=0.59",
+ "redux": "^4"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-redux/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
+ "node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "node_modules/redux-logger": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
+ "integrity": "sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg==",
+ "dependencies": {
+ "deep-diff": "^0.3.5"
+ }
+ },
+ "node_modules/redux-saga": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.3.tgz",
+ "integrity": "sha512-HDe0wTR5nhd8Xr5xjGzoyTbdAw6rjy1GDplFt3JKtKN8/MnkQSRqK/n6aQQhpw5NI4ekDVOaW+w4sdxPBaCoTQ==",
+ "dependencies": {
+ "@redux-saga/core": "^1.2.3"
+ }
+ },
+ "node_modules/redux-thunk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
+ "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
+ "peerDependencies": {
+ "redux": "^4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
+ "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/remove-accents": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
+ "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
+ },
+ "node_modules/resolve": {
+ "version": "1.22.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+ "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+ "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/seedrandom": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
+ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
+ },
+ "node_modules/semver": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+ "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz",
+ "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "get-intrinsic": "^1.1.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.3",
+ "regexp.prototype.flags": "^1.4.1",
+ "side-channel": "^1.0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
+ "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.19.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
+ "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.19.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
+ "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/svgsaver": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/svgsaver/-/svgsaver-0.9.0.tgz",
+ "integrity": "sha512-m94bg62CjuKyhcyJV50ljIPDo5FxEwmdOn60IvHEPlGKhC8gNMnyxbjlYmGi9QW9rIi93DjvfjBuafFfn3+m0w==",
+ "dependencies": {
+ "computed-styles": "^1.1.2",
+ "file-saver": "^1.3.3"
+ }
+ },
+ "node_modules/synckit": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
+ "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
+ "dev": true,
+ "dependencies": {
+ "@pkgr/utils": "^2.3.1",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/tabbable": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
+ "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/tiny-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+ },
+ "node_modules/tiny-glob": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
+ "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
+ "dev": true,
+ "dependencies": {
+ "globalyzer": "0.1.0",
+ "globrex": "^0.1.2"
+ }
+ },
+ "node_modules/tippy.js": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-5.1.2.tgz",
+ "integrity": "sha512-Qtrv2wqbRbaKMUb6bWWBQWPayvcDKNrGlvihxtsyowhT7RLGEh1STWuy6EMXC6QLkfKPB2MLnf8W2mzql9VDAw==",
+ "dependencies": {
+ "popper.js": "^1.16.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
+ "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
+ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
+ },
+ "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/tsutils/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/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-function": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.0.tgz",
+ "integrity": "sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg==",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+ "dev": true,
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-compare": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
+ "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==",
+ "dependencies": {
+ "typescript-logic": "^0.0.0"
+ }
+ },
+ "node_modules/typescript-logic": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz",
+ "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q=="
+ },
+ "node_modules/typescript-tuple": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz",
+ "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==",
+ "dependencies": {
+ "typescript-compare": "^0.0.2"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unfetch": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="
+ },
+ "node_modules/unload": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
+ "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
+ "dependencies": {
+ "@babel/runtime": "^7.6.2",
+ "detect-node": "^2.0.4"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-resize-observer": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz",
+ "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==",
+ "dependencies": {
+ "@juggle/resize-observer": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": "16.8.0 - 18",
+ "react-dom": "16.8.0 - 18"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+ "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/victory-area": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-area/-/victory-area-36.6.8.tgz",
+ "integrity": "sha512-aIyMuzUqiDcpTCB7FUOYDJvqiDPiluEXLOw6Lh1vrUYmV7CNqMDOIBtTau2vI41Ao0o0YJdCAcyzBib9e3UYbw==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8",
+ "victory-vendor": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-axis": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-axis/-/victory-axis-36.6.8.tgz",
+ "integrity": "sha512-tClVJEay1YOJAh9rRyyLx8pei7Sr1/xTz04bJmfzFoAxFoPBtvgfFwXhfZ1YjGIl7m5Wh2CiYMY3figueLzYtg==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-bar": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-bar/-/victory-bar-36.6.8.tgz",
+ "integrity": "sha512-jLLPm3IW8/2uSLPvQD9bxzXnTraUYBIDTkbZPZy7oHP01OVzP1sj+MMHcINCWcUbyUyLZDL3u8CvViXjS273JQ==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8",
+ "victory-vendor": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-brush-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-brush-container/-/victory-brush-container-36.6.8.tgz",
+ "integrity": "sha512-PN5zQ6kjVwZca1qV41WlV6J2IEyQh+2hykRe6c/wERDotVVbSrX3sJVAzUbN+7x2rrK0CL6a/XUI8jDsWTMM2A==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-chart": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-chart/-/victory-chart-36.6.8.tgz",
+ "integrity": "sha512-kC1jL63PAmqUrvZNOfwAXNuaIwz4nvXYUuEPu59WRBCOIGDGRgv2wJ1O7O0xYXqDkI57EtAYf9KUK+miEn/Btg==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-axis": "^36.6.8",
+ "victory-core": "^36.6.8",
+ "victory-polar-axis": "^36.6.8",
+ "victory-shared-events": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-core": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-core/-/victory-core-36.6.8.tgz",
+ "integrity": "sha512-SkyEszZKGyxjqfptfFWYdI22CvCuE9LhkaDpikzIhT2gcE3SuOBO5fk/740XMYE2ZUsJ4Fu/Vy4+8jZi17y44A==",
+ "dependencies": {
+ "lodash": "^4.17.21",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-vendor": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-create-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-create-container/-/victory-create-container-36.6.8.tgz",
+ "integrity": "sha512-H2BsdTbJ/RxxcEg5lzk3TDlihtOs7I/5KaIBP3yosPs702i40mL2qndkRkj08QeiZhkaKfQ2GOUvyP+t7DSdmg==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "victory-brush-container": "^36.6.8",
+ "victory-core": "^36.6.8",
+ "victory-cursor-container": "^36.6.8",
+ "victory-selection-container": "^36.6.8",
+ "victory-voronoi-container": "^36.6.8",
+ "victory-zoom-container": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-cursor-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-cursor-container/-/victory-cursor-container-36.6.8.tgz",
+ "integrity": "sha512-3WIBRl+7jnZok6syLfW8RK23nliDcoD/JUTN0YZo6bKBqHeFc4+ur3mlwCfghH7sGoxJRYuOJxTd9x2MwM5HQQ==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-errorbar": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-errorbar/-/victory-errorbar-36.6.8.tgz",
+ "integrity": "sha512-N4JdBy5wV+KU6pus7FBx+5on31oXanO+qVmtRH8u4W7CMWH5EwHortyu2wVYD9K2QoluXemIxZd7kfn14hmqfQ==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-group": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-group/-/victory-group-36.6.8.tgz",
+ "integrity": "sha512-CiupDIGPPWVgwif3ayd8glSlR41mVbuT0Nl0ay9q42w2fiM32syiJAoifIw47X4tL8ow/DXH+/5Pd8eEyA2trA==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8",
+ "victory-shared-events": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-legend": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-legend/-/victory-legend-36.6.8.tgz",
+ "integrity": "sha512-OnkzB82Mvt5/1LYNsrfZQoXaVvgfp1rCsFRI3imq257Sh/UPy0/eZehCMQs/SVbU0z0EuIpXokhZb3BBdoJgpw==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-line": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-line/-/victory-line-36.6.8.tgz",
+ "integrity": "sha512-MozOejQRZPdzFaru5zUfqVB4TEff6nZjtQhOs+F5yyhXjLgM89zGX30r3jK5cjVdAPbTu4KPUrwktvlw+AkPRA==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8",
+ "victory-vendor": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-pie": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-pie/-/victory-pie-36.6.8.tgz",
+ "integrity": "sha512-dUHWiiKd60dlt7OjFa+YYwanHAkP/T0abzy6O3SFxGre52oeqd8px1EoVhlLKpn4ao8L35koG9mvz6/pGyr8Dw==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8",
+ "victory-vendor": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-polar-axis": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-polar-axis/-/victory-polar-axis-36.6.8.tgz",
+ "integrity": "sha512-aU+Wp5six21POhI9oXeREnZHljpqcmwFHHnliVGrwgRsuc7TAjfXPWVOX9guEFfh6zQW6IZWWWTTLAN/PIEm9w==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-scatter": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-scatter/-/victory-scatter-36.6.8.tgz",
+ "integrity": "sha512-GKSNneBxIWLsF3eBSTW5IwT5S4YdsfFl4PVCP3/wTa2myfS5DIS9FufEnJp/FEZGalEXYWxeR47rlWqABxAj5A==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-selection-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-selection-container/-/victory-selection-container-36.6.8.tgz",
+ "integrity": "sha512-kudYbSX+o7fr64oeN7+EG/c+lqO22aypxVdCwa6BagAGoqqLR4jXxTqqIdp8tvxCgfCCXxopnTKYr46nubypGw==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-shared-events": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-shared-events/-/victory-shared-events-36.6.8.tgz",
+ "integrity": "sha512-hWPOVqMD3Sv6Rl1iyO6ibQrwYF9/eLCnRo0T59/Hsid6On0AJJjL9gv0oEIM5fqz7R7zx9PJmMk877IctEOemw==",
+ "dependencies": {
+ "json-stringify-safe": "^5.0.1",
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-stack": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-stack/-/victory-stack-36.6.8.tgz",
+ "integrity": "sha512-Pkux46IqAealOi0KvqQpaJKKKpHCfZ/sh5IeUKYFy+QKWAjiQjG6hFZeHgr2YaS7OfdbvHhoAdvp03KntWzpbw==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8",
+ "victory-shared-events": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-tooltip": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-tooltip/-/victory-tooltip-36.6.8.tgz",
+ "integrity": "sha512-9P+QeAGyDpP0trJnQ1NtnbDhpoJB0Ghc2boYEehvL12p0OzolY9/Nq5SDP0tu5i1BBujwFXtnoCDqt+mOH25fA==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.8.tgz",
+ "integrity": "sha512-H3kyQ+2zgjMPvbPqAl7Vwm2FD5dU7/4bCTQakFQnpIsfDljeOMDojRsrmJfwh4oAlNnWhpAf+mbAoLh8u7dwyQ==",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/victory-voronoi-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-voronoi-container/-/victory-voronoi-container-36.6.8.tgz",
+ "integrity": "sha512-x9/OOZdMm4dh38jNhSfBYT0nG6ribsINU0/WNzIn3QcDXFBInsJ7jRySxYmdmk45OdXfbDRwDMqVHk72sWQyUw==",
+ "dependencies": {
+ "delaunay-find": "0.0.6",
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8",
+ "victory-tooltip": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/victory-zoom-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-zoom-container/-/victory-zoom-container-36.6.8.tgz",
+ "integrity": "sha512-gxX5iJUaxrFFZ2IGS0sQnUI+3Mhj6bVLqtOlQd3Krld+9f/ieuUbxl+P+eIyhQU/VyHSlirIZeOGOXJeYcU9jQ==",
+ "dependencies": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
+ "node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
+ "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ },
+ "dependencies": {
+ "@auth0/auth0-react": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-1.12.1.tgz",
+ "integrity": "sha512-8+ecK/4rE0AGsxLW2IDcr1oPbT55tuE6cQEzEIOkQjB6QGQxxWMzQy0D4nMKw3JUAc7nYcFVOABNFNbc471n9Q==",
+ "requires": {
+ "@auth0/auth0-spa-js": "^1.22.6"
+ }
+ },
+ "@auth0/auth0-spa-js": {
+ "version": "1.22.6",
+ "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-1.22.6.tgz",
+ "integrity": "sha512-iL3O0vWanfKFVgy1J2ZHDPlAUK6EVHWEHWS6mUXwHEuPiK39tjlQtyUKQIJI1F5YsZB75ijGgRWMTawSDXlwCA==",
+ "requires": {
+ "abortcontroller-polyfill": "^1.7.3",
+ "browser-tabs-lock": "^1.2.15",
+ "core-js": "^3.25.4",
+ "es-cookie": "~1.3.2",
+ "fast-text-encoding": "^1.0.6",
+ "promise-polyfill": "^8.2.3",
+ "unfetch": "^4.2.0"
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
+ "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
+ "requires": {
+ "regenerator-runtime": "^0.13.11"
+ }
+ },
+ "@babel/runtime-corejs3": {
+ "version": "7.19.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.6.tgz",
+ "integrity": "sha512-oWNn1ZlGde7b4i/3tnixpH9qI0bOAACiUs+KEES4UUCnsPjVWFlWdLV/iwJuPC2qp3EowbAqsm+0XqNwnwYhxA==",
+ "dev": true,
+ "requires": {
+ "core-js-pure": "^3.25.1",
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@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.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.1.tgz",
+ "integrity": "sha512-BISJ6ZE4xQsuL/FmsyRaiffpq977bMlsKfGHTQrOGFErfByxIe6iZTxPf/00Zon9b9a7iUykfQwejN3s2ZW/Bw==",
+ "dev": true
+ },
+ "@eslint/eslintrc": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz",
+ "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.5.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ }
+ },
+ "@eslint/js": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz",
+ "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==",
+ "dev": true
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+ "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "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==",
+ "dev": true
+ },
+ "@juggle/resize-observer": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
+ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
+ },
+ "@next/env": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz",
+ "integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ=="
+ },
+ "@next/eslint-plugin-next": {
+ "version": "13.2.4",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.4.tgz",
+ "integrity": "sha512-ck1lI+7r1mMJpqLNa3LJ5pxCfOB1lfJncKmRJeJxcJqcngaFwylreLP7da6Rrjr6u2gVRTfmnkSkjc80IiQCwQ==",
+ "dev": true,
+ "requires": {
+ "glob": "7.1.7"
+ }
+ },
+ "@next/swc-darwin-arm64": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz",
+ "integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==",
+ "optional": true
+ },
+ "@next/swc-darwin-x64": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz",
+ "integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==",
+ "optional": true
+ },
+ "@next/swc-linux-arm64-gnu": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz",
+ "integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==",
+ "optional": true
+ },
+ "@next/swc-linux-arm64-musl": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz",
+ "integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==",
+ "optional": true
+ },
+ "@next/swc-linux-x64-gnu": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz",
+ "integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==",
+ "optional": true
+ },
+ "@next/swc-linux-x64-musl": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz",
+ "integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==",
+ "optional": true
+ },
+ "@next/swc-win32-arm64-msvc": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz",
+ "integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==",
+ "optional": true
+ },
+ "@next/swc-win32-ia32-msvc": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz",
+ "integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==",
+ "optional": true
+ },
+ "@next/swc-win32-x64-msvc": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz",
+ "integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==",
+ "optional": true
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@patternfly/react-charts": {
+ "version": "6.94.18",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-charts/-/react-charts-6.94.18.tgz",
+ "integrity": "sha512-56WxnZYC3blRX41mW67JaPxJ3YhXViLvwGpEsZrYCccla/rTV8JgKK0gjHnqtzPQiVBfpn+3ewOyNCOR5uRoSw==",
+ "requires": {
+ "@patternfly/react-styles": "^4.92.6",
+ "@patternfly/react-tokens": "^4.94.6",
+ "hoist-non-react-statics": "^3.3.0",
+ "lodash": "^4.17.19",
+ "tslib": "^2.0.0",
+ "victory-area": "^36.6.7",
+ "victory-axis": "^36.6.7",
+ "victory-bar": "^36.6.7",
+ "victory-chart": "^36.6.7",
+ "victory-core": "^36.6.7",
+ "victory-create-container": "^36.6.7",
+ "victory-cursor-container": "^36.6.7",
+ "victory-group": "^36.6.7",
+ "victory-legend": "^36.6.7",
+ "victory-line": "^36.6.7",
+ "victory-pie": "^36.6.7",
+ "victory-scatter": "^36.6.7",
+ "victory-stack": "^36.6.7",
+ "victory-tooltip": "^36.6.7",
+ "victory-voronoi-container": "^36.6.7",
+ "victory-zoom-container": "^36.6.7"
+ }
+ },
+ "@patternfly/react-core": {
+ "version": "4.276.6",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.276.6.tgz",
+ "integrity": "sha512-G0K+378jf9jw9g+hCAoKnsAe/UGTRspqPeuAYypF2FgNr+dC7dUpc7/VkNhZBVqSJzUWVEK8NyXcqkfi0IemIg==",
+ "requires": {
+ "@patternfly/react-icons": "^4.93.6",
+ "@patternfly/react-styles": "^4.92.6",
+ "@patternfly/react-tokens": "^4.94.6",
+ "focus-trap": "6.9.2",
+ "react-dropzone": "9.0.0",
+ "tippy.js": "5.1.2",
+ "tslib": "^2.0.0"
+ }
+ },
+ "@patternfly/react-icons": {
+ "version": "4.93.6",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.93.6.tgz",
+ "integrity": "sha512-ZrXegc/81oiuTIeWvoHb3nG/eZODbB4rYmekBEsrbiysyO7m/sUFoi/RLvgFINrRoh6YCJqL5fj06Jg6d7jX1g==",
+ "requires": {}
+ },
+ "@patternfly/react-styles": {
+ "version": "4.92.6",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.92.6.tgz",
+ "integrity": "sha512-b8uQdEReMyeoMzjpMri845QxqtupY/tIFFYfVeKoB2neno8gkcW1RvDdDe62LF88q45OktCwAe/8A99ker10Iw=="
+ },
+ "@patternfly/react-table": {
+ "version": "4.112.39",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.112.39.tgz",
+ "integrity": "sha512-U+hOMgYlbghGH4M5MX+qt0GkVi/ocrGnxDnm11YiS3CtEGsd6Rr0NeqMmk0uoR46Od4Pr5tKuXxZhPP32sCL/w==",
+ "requires": {
+ "@patternfly/react-core": "^4.276.6",
+ "@patternfly/react-icons": "^4.93.6",
+ "@patternfly/react-styles": "^4.92.6",
+ "@patternfly/react-tokens": "^4.94.6",
+ "lodash": "^4.17.19",
+ "tslib": "^2.0.0"
+ }
+ },
+ "@patternfly/react-tokens": {
+ "version": "4.94.6",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.94.6.tgz",
+ "integrity": "sha512-tm7C6nat+uKMr1hrapis7hS3rN9cr41tpcCKhx6cod6FLU8KwF2Yt5KUxakhIOCEcE/M/EhXhAw/qejp8w0r7Q=="
+ },
+ "@pkgr/utils": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
+ "integrity": "sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "is-glob": "^4.0.3",
+ "open": "^8.4.0",
+ "picocolors": "^1.0.0",
+ "tiny-glob": "^0.2.9",
+ "tslib": "^2.4.0"
+ }
+ },
+ "@redux-saga/core": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.3.tgz",
+ "integrity": "sha512-U1JO6ncFBAklFTwoQ3mjAeQZ6QGutsJzwNBjgVLSWDpZTRhobUzuVDS1qH3SKGJD8fvqoaYOjp6XJ3gCmeZWgA==",
+ "requires": {
+ "@babel/runtime": "^7.6.3",
+ "@redux-saga/deferred": "^1.2.1",
+ "@redux-saga/delay-p": "^1.2.1",
+ "@redux-saga/is": "^1.1.3",
+ "@redux-saga/symbols": "^1.1.3",
+ "@redux-saga/types": "^1.2.1",
+ "redux": "^4.0.4",
+ "typescript-tuple": "^2.2.1"
+ }
+ },
+ "@redux-saga/deferred": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz",
+ "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g=="
+ },
+ "@redux-saga/delay-p": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz",
+ "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==",
+ "requires": {
+ "@redux-saga/symbols": "^1.1.3"
+ }
+ },
+ "@redux-saga/is": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz",
+ "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==",
+ "requires": {
+ "@redux-saga/symbols": "^1.1.3",
+ "@redux-saga/types": "^1.2.1"
+ }
+ },
+ "@redux-saga/symbols": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz",
+ "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg=="
+ },
+ "@redux-saga/types": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz",
+ "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA=="
+ },
+ "@rushstack/eslint-patch": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
+ "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==",
+ "dev": true
+ },
+ "@sentry-internal/tracing": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.45.0.tgz",
+ "integrity": "sha512-0aIDY2OvUX7k2XHaimOlWkboXoQvJ9dEKvfpu0Wh0YxfUTGPa+wplUdg3WVdkk018sq1L11MKmj4MPZyYUvXhw==",
+ "requires": {
+ "@sentry/core": "7.45.0",
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0",
+ "tslib": "^1.9.3"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ }
+ }
+ },
+ "@sentry/browser": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.45.0.tgz",
+ "integrity": "sha512-/dUrUwnI34voMj+jSJT7b5Jun+xy1utVyzzwTq3Oc22N+SB17ZOX9svZ4jl1Lu6tVJPVjPyvL6zlcbrbMwqFjg==",
+ "requires": {
+ "@sentry-internal/tracing": "7.45.0",
+ "@sentry/core": "7.45.0",
+ "@sentry/replay": "7.45.0",
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0",
+ "tslib": "^1.9.3"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ }
+ }
+ },
+ "@sentry/core": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.45.0.tgz",
+ "integrity": "sha512-xJfdTS4lRmHvZI/A5MazdnKhBJFkisKu6G9EGNLlZLre+6W4PH5sb7QX4+xoBdqG7v10Jvdia112vi762ojO2w==",
+ "requires": {
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0",
+ "tslib": "^1.9.3"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ }
+ }
+ },
+ "@sentry/react": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.45.0.tgz",
+ "integrity": "sha512-Dbz85nfvMUikbLHUuIt6fBNPmTvThFn+rWB5KS1NIOJifyWAdpIU3X7yCUJE5xhsUObNLiHlNJlqhaQI4nR1bQ==",
+ "requires": {
+ "@sentry/browser": "7.45.0",
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0",
+ "hoist-non-react-statics": "^3.3.2",
+ "tslib": "^1.9.3"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ }
+ }
+ },
+ "@sentry/replay": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.45.0.tgz",
+ "integrity": "sha512-smM7FIcFIyKu30BqCl8BzLo1gH/z9WwXdGX6V0fNvHab9fJZ09+xjFn+LmIyo6N8H8jjwsup0+yQ12kiF/ZsEw==",
+ "requires": {
+ "@sentry/core": "7.45.0",
+ "@sentry/types": "7.45.0",
+ "@sentry/utils": "7.45.0"
+ }
+ },
+ "@sentry/tracing": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.45.0.tgz",
+ "integrity": "sha512-FsoFmZPzTBGvWeJH73NxSF1ot61Zw3aIZo5XolengiKnRmcrQOFxebtMKBiZ61QBRYGqsm5uT7QB7zITU6Ikgg==",
+ "requires": {
+ "@sentry-internal/tracing": "7.45.0"
+ }
+ },
+ "@sentry/types": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.45.0.tgz",
+ "integrity": "sha512-iFt7msfUK8LCodFF3RKUyaxy9tJv/gpWhzxUFyNxtuVwlpmd+q6mtsFGn8Af3pbpm8A+MKyz1ebMwXj0PQqknw=="
+ },
+ "@sentry/utils": {
+ "version": "7.45.0",
+ "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.45.0.tgz",
+ "integrity": "sha512-aTY7qqtNUudd09SH5DVSKMm3iQ6ZeWufduc0I9bPZe6UMM09BDc4KmjmrzRkdQ+VaOmHo7+v+HZKQk5f+AbuTQ==",
+ "requires": {
+ "@sentry/types": "7.45.0",
+ "tslib": "^1.9.3"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ }
+ }
+ },
+ "@swc/helpers": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
+ "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
+ "requires": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "@types/d3-array": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz",
+ "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ=="
+ },
+ "@types/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA=="
+ },
+ "@types/d3-ease": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz",
+ "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA=="
+ },
+ "@types/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==",
+ "requires": {
+ "@types/d3-color": "*"
+ }
+ },
+ "@types/d3-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz",
+ "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg=="
+ },
+ "@types/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==",
+ "requires": {
+ "@types/d3-time": "*"
+ }
+ },
+ "@types/d3-shape": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.0.tgz",
+ "integrity": "sha512-jYIYxFFA9vrJ8Hd4Se83YI6XF+gzDL1aC5DCsldai4XYYiVNdhtpGbA/GM6iyQ8ayhSp3a148LY34hy7A4TxZA==",
+ "requires": {
+ "@types/d3-path": "*"
+ }
+ },
+ "@types/d3-time": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
+ "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg=="
+ },
+ "@types/d3-timer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz",
+ "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g=="
+ },
+ "@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+ "requires": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
+ "@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "@types/prop-types": {
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+ },
+ "@types/react": {
+ "version": "18.0.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.23.tgz",
+ "integrity": "sha512-R1wTULtCiJkudAN2DJGoYYySbGtOdzZyUWAACYinKdiQC8auxso4kLDUhQ7AJ2kh3F6A6z4v69U6tNY39hihVQ==",
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/react-reconciler": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.2.tgz",
+ "integrity": "sha512-8tu6lHzEgYPlfDf/J6GOQdIc+gs+S2yAqlby3zTsB3SP2svlqTYe5fwZNtZyfactP74ShooP2vvi1BOp9ZemWw==",
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+ },
+ "@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
+ "@typescript-eslint/parser": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.1.tgz",
+ "integrity": "sha512-kAV+NiNBWVQDY9gDJDToTE/NO8BHi4f6b7zTsVAJoTkmB/zlfOpiEVBzHOKtlgTndCKe8vj9F/PuolemZSh50Q==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/scope-manager": "5.42.1",
+ "@typescript-eslint/types": "5.42.1",
+ "@typescript-eslint/typescript-estree": "5.42.1",
+ "debug": "^4.3.4"
+ }
+ },
+ "@typescript-eslint/scope-manager": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.1.tgz",
+ "integrity": "sha512-QAZY/CBP1Emx4rzxurgqj3rUinfsh/6mvuKbLNMfJMMKYLRBfweus8brgXF8f64ABkIZ3zdj2/rYYtF8eiuksQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.42.1",
+ "@typescript-eslint/visitor-keys": "5.42.1"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.1.tgz",
+ "integrity": "sha512-Qrco9dsFF5lhalz+lLFtxs3ui1/YfC6NdXu+RAGBa8uSfn01cjO7ssCsjIsUs484vny9Xm699FSKwpkCcqwWwA==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.1.tgz",
+ "integrity": "sha512-qElc0bDOuO0B8wDhhW4mYVgi/LZL+igPwXtV87n69/kYC/7NG3MES0jHxJNCr4EP7kY1XVsRy8C/u3DYeTKQmw==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.42.1",
+ "@typescript-eslint/visitor-keys": "5.42.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "5.42.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.1.tgz",
+ "integrity": "sha512-LOQtSF4z+hejmpUvitPlc4hA7ERGoj2BVkesOcG91HCn8edLGUXbTrErmutmPbl8Bo9HjAvOO/zBKQHExXNA2A==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.42.1",
+ "eslint-visitor-keys": "^3.3.0"
+ }
+ },
+ "abortcontroller-polyfill": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz",
+ "integrity": "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ=="
+ },
+ "acorn": {
+ "version": "8.8.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+ "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "requires": {}
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "optional": true,
+ "peer": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "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
+ },
+ "aria-query": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
+ "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.10.2",
+ "@babel/runtime-corejs3": "^7.10.2"
+ }
+ },
+ "array-includes": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+ "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ }
+ },
+ "array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true
+ },
+ "array.prototype.flat": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+ "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ }
+ },
+ "array.prototype.flatmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
+ "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ }
+ },
+ "ast-types-flow": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
+ "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==",
+ "dev": true
+ },
+ "attr-accept": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz",
+ "integrity": "sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==",
+ "requires": {
+ "core-js": "^2.5.0"
+ },
+ "dependencies": {
+ "core-js": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
+ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="
+ }
+ }
+ },
+ "axe-core": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.0.tgz",
+ "integrity": "sha512-4+rr8eQ7+XXS5nZrKcMO/AikHL0hVqy+lHWAnE3xdHl+aguag8SOQ6eEqLexwLNWgXIMfunGuD3ON1/6Kyet0A==",
+ "dev": true
+ },
+ "axobject-query": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
+ "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "optional": true,
+ "peer": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "devOptional": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "broadcast-channel": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
+ "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "detect-node": "^2.1.0",
+ "js-sha3": "0.8.0",
+ "microseconds": "0.2.0",
+ "nano-time": "1.0.0",
+ "oblivious-set": "1.0.0",
+ "rimraf": "3.0.2",
+ "unload": "2.2.0"
+ }
+ },
+ "browser-tabs-lock": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/browser-tabs-lock/-/browser-tabs-lock-1.2.15.tgz",
+ "integrity": "sha512-J8K9vdivK0Di+b8SBdE7EZxDr88TnATing7XoLw6+nFkXMQ6sVBh92K3NQvZlZU91AIkFRi0w3sztk5Z+vsswA==",
+ "requires": {
+ "lodash": ">=4.17.21"
+ }
+ },
+ "busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "requires": {
+ "streamsearch": "^1.1.0"
+ }
+ },
+ "call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001426",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001426.tgz",
+ "integrity": "sha512-n7cosrHLl8AWt0wwZw/PJZgUg3lV0gk9LMI7ikGJwhyhgsd2Nb65vKvmSexCqq/J7rbH3mFG6yZZiPR5dLPW5A=="
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "optional": true,
+ "peer": true,
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "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==",
+ "optional": true,
+ "peer": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ }
+ }
+ },
+ "client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
+ },
+ "clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ },
+ "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==",
+ "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=="
+ },
+ "complex.js": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz",
+ "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg=="
+ },
+ "computed-styles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/computed-styles/-/computed-styles-1.1.2.tgz",
+ "integrity": "sha512-CGbti1B791SKg6goVX0cSI++hFBSzY9+7+lhX8lqXDI5FHexluglI1cPtvIifS4mEcWxPJ+HKYPr2t6nqz7PxA=="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "core-js": {
+ "version": "3.29.1",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz",
+ "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw=="
+ },
+ "core-js-pure": {
+ "version": "3.26.0",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.0.tgz",
+ "integrity": "sha512-LiN6fylpVBVwT8twhhluD9TzXmZQQsr2I2eIKtWNbZI1XMfBT7CV18itaN6RA7EtQd/SDdRx/wzvAShX2HvhQA==",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "csstype": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
+ "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
+ },
+ "d3-array": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz",
+ "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==",
+ "requires": {
+ "internmap": "1 - 2"
+ }
+ },
+ "d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="
+ },
+ "d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="
+ },
+ "d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="
+ },
+ "d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "requires": {
+ "d3-color": "1 - 3"
+ }
+ },
+ "d3-path": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz",
+ "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w=="
+ },
+ "d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "requires": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ }
+ },
+ "d3-shape": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz",
+ "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==",
+ "requires": {
+ "d3-path": "1 - 3"
+ }
+ },
+ "d3-time": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz",
+ "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==",
+ "requires": {
+ "d3-array": "2 - 3"
+ }
+ },
+ "d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "requires": {
+ "d3-time": "1 - 3"
+ }
+ },
+ "d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="
+ },
+ "damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "decimal.js": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
+ },
+ "deep-diff": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
+ "integrity": "sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug=="
+ },
+ "deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
+ "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
+ "dev": true,
+ "requires": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "delaunator": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
+ "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag=="
+ },
+ "delaunay-find": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/delaunay-find/-/delaunay-find-0.0.6.tgz",
+ "integrity": "sha512-1+almjfrnR7ZamBk0q3Nhg6lqSe6Le4vL0WJDSMx4IDbQwTpUTXPjxC00lqLBT8MYsJpPCbI16sIkw9cPsbi7Q==",
+ "requires": {
+ "delaunator": "^4.0.0"
+ }
+ },
+ "detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
+ },
+ "dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "requires": {
+ "path-type": "^4.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "enhanced-resolve": {
+ "version": "5.12.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
+ "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ }
+ },
+ "es-abstract": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz",
+ "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.1.3",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.2",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trimend": "^1.0.5",
+ "string.prototype.trimstart": "^1.0.5",
+ "unbox-primitive": "^1.0.2"
+ }
+ },
+ "es-cookie": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz",
+ "integrity": "sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q=="
+ },
+ "es-shim-unscopables": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+ "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escape-latex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
+ "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "eslint": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz",
+ "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==",
+ "dev": true,
+ "requires": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.4.0",
+ "@eslint/eslintrc": "^2.0.1",
+ "@eslint/js": "8.36.0",
+ "@humanwhocodes/config-array": "^0.11.8",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.1",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.5.0",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "grapheme-splitter": "^1.0.4",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-sdsl": "^4.1.4",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0"
+ }
+ },
+ "eslint-config-next": {
+ "version": "13.2.4",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.2.4.tgz",
+ "integrity": "sha512-lunIBhsoeqw6/Lfkd6zPt25w1bn0znLA/JCL+au1HoEpSb4/PpsOYsYtgV/q+YPsoKIOzFyU5xnb04iZnXjUvg==",
+ "dev": true,
+ "requires": {
+ "@next/eslint-plugin-next": "13.2.4",
+ "@rushstack/eslint-patch": "^1.1.3",
+ "@typescript-eslint/parser": "^5.42.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-import-resolver-typescript": "^3.5.2",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
+ "eslint-plugin-react": "^7.31.7",
+ "eslint-plugin-react-hooks": "^4.5.0"
+ }
+ },
+ "eslint-import-resolver-node": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
+ "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==",
+ "dev": true,
+ "requires": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.11.0",
+ "resolve": "^1.22.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "eslint-import-resolver-typescript": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.3.tgz",
+ "integrity": "sha512-njRcKYBc3isE42LaTcJNVANR3R99H9bAxBDMNDr2W7yq5gYPxbU3MkdhsQukxZ/Xg9C2vcyLlDsbKfRDg0QvCQ==",
+ "dev": true,
+ "requires": {
+ "debug": "^4.3.4",
+ "enhanced-resolve": "^5.10.0",
+ "get-tsconfig": "^4.2.0",
+ "globby": "^13.1.2",
+ "is-core-module": "^2.10.0",
+ "is-glob": "^4.0.3",
+ "synckit": "^0.8.4"
+ },
+ "dependencies": {
+ "globby": {
+ "version": "13.1.3",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz",
+ "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==",
+ "dev": true,
+ "requires": {
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.11",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^4.0.0"
+ }
+ },
+ "slash": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+ "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-module-utils": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz",
+ "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==",
+ "dev": true,
+ "requires": {
+ "debug": "^3.2.7"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "eslint-plugin-import": {
+ "version": "2.27.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz",
+ "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "array.prototype.flatmap": "^1.3.1",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.7",
+ "eslint-module-utils": "^2.7.4",
+ "has": "^1.0.3",
+ "is-core-module": "^2.11.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.6",
+ "resolve": "^1.22.1",
+ "semver": "^6.3.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-plugin-jsx-a11y": {
+ "version": "6.6.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz",
+ "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.18.9",
+ "aria-query": "^4.2.2",
+ "array-includes": "^3.1.5",
+ "ast-types-flow": "^0.0.7",
+ "axe-core": "^4.4.3",
+ "axobject-query": "^2.2.0",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "has": "^1.0.3",
+ "jsx-ast-utils": "^3.3.2",
+ "language-tags": "^1.0.5",
+ "minimatch": "^3.1.2",
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-plugin-react": {
+ "version": "7.31.10",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz",
+ "integrity": "sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.5",
+ "array.prototype.flatmap": "^1.3.0",
+ "doctrine": "^2.1.0",
+ "estraverse": "^5.3.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.5",
+ "object.fromentries": "^2.0.5",
+ "object.hasown": "^1.1.1",
+ "object.values": "^1.1.5",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.3",
+ "semver": "^6.3.0",
+ "string.prototype.matchall": "^4.0.7"
+ },
+ "dependencies": {
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "resolve": {
+ "version": "2.0.0-next.4",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
+ "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-plugin-react-hooks": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
+ "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
+ "dev": true,
+ "requires": {}
+ },
+ "eslint-scope": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+ "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+ "dev": true
+ },
+ "espree": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz",
+ "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.8.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.3.0"
+ }
+ },
+ "esquery": {
+ "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"
+ }
+ },
+ "esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "requires": {
+ "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
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "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==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "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": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "fast-text-encoding": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz",
+ "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="
+ },
+ "fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "dev": true,
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^3.0.4"
+ }
+ },
+ "file-saver": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz",
+ "integrity": "sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg=="
+ },
+ "file-selector": {
+ "version": "0.1.19",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz",
+ "integrity": "sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==",
+ "requires": {
+ "tslib": "^2.0.1"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "devOptional": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "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"
+ }
+ },
+ "flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.1.0",
+ "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==",
+ "dev": true
+ },
+ "focus-trap": {
+ "version": "6.9.2",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.2.tgz",
+ "integrity": "sha512-gBEuXOPNOKPrLdZpMFUSTyIo1eT2NSZRrwZ9r/0Jqw5tmT3Yvxfmu8KBHw8xW2XQkw6E/JoG+OlEq7UDtSUNgw==",
+ "requires": {
+ "tabbable": "^5.3.2"
+ }
+ },
+ "fraction.js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
+ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA=="
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "optional": true,
+ "peer": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "function.prototype.name": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+ "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ }
+ },
+ "functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true
+ },
+ "get-intrinsic": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
+ "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.3"
+ }
+ },
+ "get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "get-tsconfig": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.4.0.tgz",
+ "integrity": "sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "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.3"
+ }
+ },
+ "glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
+ },
+ "globals": {
+ "version": "13.20.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+ "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "globalyzer": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
+ "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==",
+ "dev": true
+ },
+ "globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "requires": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ }
+ },
+ "globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
+ "dev": true
+ },
+ "graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "grapheme-splitter": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "has-property-descriptors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+ "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true
+ },
+ "has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "requires": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true
+ },
+ "immer": {
+ "version": "9.0.21",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
+ "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="
+ },
+ "immutable": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
+ "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+ "optional": true,
+ "peer": true
+ },
+ "import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
+ },
+ "is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "requires": {
+ "has-bigints": "^1.0.1"
+ }
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "optional": true,
+ "peer": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true
+ },
+ "is-core-module": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+ "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "devOptional": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "devOptional": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "devOptional": true
+ },
+ "is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "requires": {
+ "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-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-shared-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
+ "is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
+ "is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "requires": {
+ "is-docker": "^2.0.0"
+ }
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "its-fine": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.0.6.tgz",
+ "integrity": "sha512-VZJZPwVT2kxe5KQv+TxCjojfLiUIut8zXDNLTxcM7gJ/xQ/bSPk5M0neZ+j3myy45KKkltY1mm1jyJgx3Fxsdg==",
+ "requires": {
+ "@types/react-reconciler": "^0.28.0"
+ }
+ },
+ "javascript-natural-sort": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
+ },
+ "js-sdsl": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
+ "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==",
+ "dev": true
+ },
+ "js-sha3": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "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"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
+ },
+ "json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "jsx-ast-utils": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
+ "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.5",
+ "object.assign": "^4.1.3"
+ }
+ },
+ "konva": {
+ "version": "8.4.3",
+ "resolved": "https://registry.npmjs.org/konva/-/konva-8.4.3.tgz",
+ "integrity": "sha512-ARqdgAbdNIougRlOKvkQwHlGhXPRBV4KvhCP+qoPpGoVQwwiJe4Hkdu4HHdRPb9rGUp04jDTAxBzEwBsE272pg=="
+ },
+ "language-subtag-registry": {
+ "version": "0.3.22",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+ "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "dev": true
+ },
+ "language-tags": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz",
+ "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==",
+ "dev": true,
+ "requires": {
+ "language-subtag-registry": "~0.3.2"
+ }
+ },
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
+ "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"
+ }
+ },
+ "lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "match-sorter": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz",
+ "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==",
+ "requires": {
+ "@babel/runtime": "^7.12.5",
+ "remove-accents": "0.4.2"
+ }
+ },
+ "mathjs": {
+ "version": "11.7.0",
+ "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.7.0.tgz",
+ "integrity": "sha512-RCXtrP5xGIbl9PUc5+7QL81rBCUjzoIZ0ugNqKsarOUxg+x7deY0BzfNai+bGfUL/T+1uYq1xs5w2xVdL3lp0g==",
+ "requires": {
+ "@babel/runtime": "^7.21.0",
+ "complex.js": "^2.1.1",
+ "decimal.js": "^10.4.3",
+ "escape-latex": "^1.2.0",
+ "fraction.js": "^4.2.0",
+ "javascript-natural-sort": "^0.7.1",
+ "seedrandom": "^3.0.5",
+ "tiny-emitter": "^2.1.0",
+ "typed-function": "^4.1.0"
+ }
+ },
+ "merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ }
+ },
+ "microseconds": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
+ "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "nano-time": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
+ "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
+ "requires": {
+ "big-integer": "^1.6.16"
+ }
+ },
+ "nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "next": {
+ "version": "13.5.4",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz",
+ "integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==",
+ "requires": {
+ "@next/env": "13.5.4",
+ "@next/swc-darwin-arm64": "13.5.4",
+ "@next/swc-darwin-x64": "13.5.4",
+ "@next/swc-linux-arm64-gnu": "13.5.4",
+ "@next/swc-linux-arm64-musl": "13.5.4",
+ "@next/swc-linux-x64-gnu": "13.5.4",
+ "@next/swc-linux-x64-musl": "13.5.4",
+ "@next/swc-win32-arm64-msvc": "13.5.4",
+ "@next/swc-win32-ia32-msvc": "13.5.4",
+ "@next/swc-win32-x64-msvc": "13.5.4",
+ "@swc/helpers": "0.5.2",
+ "busboy": "1.6.0",
+ "caniuse-lite": "^1.0.30001406",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.1",
+ "watchpack": "2.4.0"
+ }
+ },
+ "next-global-css": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/next-global-css/-/next-global-css-1.3.1.tgz",
+ "integrity": "sha512-+OnTwQKmv1lDP7r4R3T94oq6372R9UGVivchBQu49j7ZjzvSXHCnv93yAuhgMkvUgAbGifTs8sQ5YL9wjyAxfA=="
+ },
+ "next-runtime-env": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/next-runtime-env/-/next-runtime-env-1.7.1.tgz",
+ "integrity": "sha512-KIWciYVcYBoc0dBTyx7tEogyESacAJCkseU9+AcIYbkq3MiV6WyhHHPfDONpXbJSlpT3u3utrSrngua2EFlfyA==",
+ "requires": {
+ "chalk": "^4.1.2"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "optional": true,
+ "peer": true
+ },
+ "normalizr": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/normalizr/-/normalizr-3.6.2.tgz",
+ "integrity": "sha512-30qCybsBaCBciotorvuOZTCGEg2AXrJfADMT2Kk/lvpIAcipHdK0zc33nNtwKzyfQAqIJXAcqET6YgflYUgsoQ=="
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
+ },
+ "object-inspect": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
+ "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
+ "dev": true
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true
+ },
+ "object.assign": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+ "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "object.entries": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz",
+ "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "object.fromentries": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz",
+ "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "object.hasown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz",
+ "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.19.5"
+ }
+ },
+ "object.values": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+ "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ }
+ },
+ "oblivious-set": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
+ "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw=="
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "dev": true,
+ "requires": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ }
+ },
+ "optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "requires": {
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "devOptional": true
+ },
+ "popper.js": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
+ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
+ },
+ "postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "requires": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
+ },
+ "prettier": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
+ "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="
+ },
+ "promise-polyfill": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
+ "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg=="
+ },
+ "prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "prop-types-extra": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
+ "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
+ "requires": {
+ "react-is": "^16.3.2",
+ "warning": "^4.0.0"
+ }
+ },
+ "punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "dev": true
+ },
+ "queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true
+ },
+ "react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ }
+ },
+ "react-dropzone": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-9.0.0.tgz",
+ "integrity": "sha512-wZ2o9B2qkdE3RumWhfyZT9swgJYJPeU5qHEcMU8weYpmLex1eeWX0CC32/Y0VutB+BBi2D+iePV/YZIiB4kZGw==",
+ "requires": {
+ "attr-accept": "^1.1.3",
+ "file-selector": "^0.1.8",
+ "prop-types": "^15.6.2",
+ "prop-types-extra": "^1.1.0"
+ }
+ },
+ "react-fast-compare": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
+ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
+ },
+ "react-hotkeys-hook": {
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.8.tgz",
+ "integrity": "sha512-RmrIQ3M259c84MnYVEAQsmHkD6s7XUgLG0rW6S7qjt1Lh7q+SPIz5b6obVU8OJw1Utsj1mUCj6twtBPaK/ytww==",
+ "requires": {}
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "react-konva": {
+ "version": "18.2.5",
+ "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.5.tgz",
+ "integrity": "sha512-lTqJStcHnpGSXB9RlV7p5at3MpRML/TujzbuUDZRIInsLocJ/I4Nhhg3w6yJm9UV05kcwr88OY6LO+2zRyzXog==",
+ "requires": {
+ "@types/react-reconciler": "^0.28.2",
+ "its-fine": "^1.0.6",
+ "react-reconciler": "~0.29.0",
+ "scheduler": "^0.23.0"
+ }
+ },
+ "react-query": {
+ "version": "3.39.3",
+ "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
+ "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "broadcast-channel": "^3.4.1",
+ "match-sorter": "^6.0.2"
+ }
+ },
+ "react-reconciler": {
+ "version": "0.29.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz",
+ "integrity": "sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ }
+ },
+ "react-redux": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz",
+ "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==",
+ "requires": {
+ "@babel/runtime": "^7.12.1",
+ "@types/hoist-non-react-statics": "^3.3.1",
+ "@types/use-sync-external-store": "^0.0.3",
+ "hoist-non-react-statics": "^3.3.2",
+ "react-is": "^18.0.0",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ }
+ }
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "optional": true,
+ "peer": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "requires": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "redux-logger": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
+ "integrity": "sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg==",
+ "requires": {
+ "deep-diff": "^0.3.5"
+ }
+ },
+ "redux-saga": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.3.tgz",
+ "integrity": "sha512-HDe0wTR5nhd8Xr5xjGzoyTbdAw6rjy1GDplFt3JKtKN8/MnkQSRqK/n6aQQhpw5NI4ekDVOaW+w4sdxPBaCoTQ==",
+ "requires": {
+ "@redux-saga/core": "^1.2.3"
+ }
+ },
+ "redux-thunk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
+ "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
+ "requires": {}
+ },
+ "regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
+ },
+ "regexp.prototype.flags": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
+ "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "functions-have-names": "^1.2.2"
+ }
+ },
+ "remove-accents": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
+ "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
+ },
+ "resolve": {
+ "version": "1.22.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+ "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "safe-regex-test": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+ "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ }
+ },
+ "sass": {
+ "version": "1.55.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz",
+ "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==",
+ "optional": true,
+ "peer": true,
+ "requires": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ }
+ },
+ "scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "seedrandom": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
+ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
+ },
+ "semver": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+ "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
+ "side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
+ "slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
+ },
+ "streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
+ },
+ "string.prototype.matchall": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz",
+ "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "get-intrinsic": "^1.1.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.3",
+ "regexp.prototype.flags": "^1.4.1",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
+ "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.19.5"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
+ "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.19.5"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
+ },
+ "styled-jsx": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
+ "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
+ "requires": {
+ "client-only": "0.0.1"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true
+ },
+ "svgsaver": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/svgsaver/-/svgsaver-0.9.0.tgz",
+ "integrity": "sha512-m94bg62CjuKyhcyJV50ljIPDo5FxEwmdOn60IvHEPlGKhC8gNMnyxbjlYmGi9QW9rIi93DjvfjBuafFfn3+m0w==",
+ "requires": {
+ "computed-styles": "^1.1.2",
+ "file-saver": "^1.3.3"
+ }
+ },
+ "synckit": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
+ "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
+ "dev": true,
+ "requires": {
+ "@pkgr/utils": "^2.3.1",
+ "tslib": "^2.5.0"
+ }
+ },
+ "tabbable": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
+ "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
+ },
+ "tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "tiny-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+ },
+ "tiny-glob": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
+ "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
+ "dev": true,
+ "requires": {
+ "globalyzer": "0.1.0",
+ "globrex": "^0.1.2"
+ }
+ },
+ "tippy.js": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-5.1.2.tgz",
+ "integrity": "sha512-Qtrv2wqbRbaKMUb6bWWBQWPayvcDKNrGlvihxtsyowhT7RLGEh1STWuy6EMXC6QLkfKPB2MLnf8W2mzql9VDAw==",
+ "requires": {
+ "popper.js": "^1.16.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "devOptional": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "tsconfig-paths": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
+ "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
+ "dev": true,
+ "requires": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "tslib": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
+ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
+ },
+ "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"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ }
+ }
+ },
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true
+ },
+ "typed-function": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.0.tgz",
+ "integrity": "sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg=="
+ },
+ "typescript": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
+ "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
+ "dev": true,
+ "peer": true
+ },
+ "typescript-compare": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
+ "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==",
+ "requires": {
+ "typescript-logic": "^0.0.0"
+ }
+ },
+ "typescript-logic": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz",
+ "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q=="
+ },
+ "typescript-tuple": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz",
+ "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==",
+ "requires": {
+ "typescript-compare": "^0.0.2"
+ }
+ },
+ "unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ }
+ },
+ "unfetch": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="
+ },
+ "unload": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
+ "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
+ "requires": {
+ "@babel/runtime": "^7.6.2",
+ "detect-node": "^2.0.4"
+ }
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "use-resize-observer": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz",
+ "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==",
+ "requires": {
+ "@juggle/resize-observer": "^3.3.1"
+ }
+ },
+ "use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "requires": {}
+ },
+ "uuid": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+ "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
+ },
+ "victory-area": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-area/-/victory-area-36.6.8.tgz",
+ "integrity": "sha512-aIyMuzUqiDcpTCB7FUOYDJvqiDPiluEXLOw6Lh1vrUYmV7CNqMDOIBtTau2vI41Ao0o0YJdCAcyzBib9e3UYbw==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8",
+ "victory-vendor": "^36.6.8"
+ }
+ },
+ "victory-axis": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-axis/-/victory-axis-36.6.8.tgz",
+ "integrity": "sha512-tClVJEay1YOJAh9rRyyLx8pei7Sr1/xTz04bJmfzFoAxFoPBtvgfFwXhfZ1YjGIl7m5Wh2CiYMY3figueLzYtg==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-bar": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-bar/-/victory-bar-36.6.8.tgz",
+ "integrity": "sha512-jLLPm3IW8/2uSLPvQD9bxzXnTraUYBIDTkbZPZy7oHP01OVzP1sj+MMHcINCWcUbyUyLZDL3u8CvViXjS273JQ==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8",
+ "victory-vendor": "^36.6.8"
+ }
+ },
+ "victory-brush-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-brush-container/-/victory-brush-container-36.6.8.tgz",
+ "integrity": "sha512-PN5zQ6kjVwZca1qV41WlV6J2IEyQh+2hykRe6c/wERDotVVbSrX3sJVAzUbN+7x2rrK0CL6a/XUI8jDsWTMM2A==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-chart": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-chart/-/victory-chart-36.6.8.tgz",
+ "integrity": "sha512-kC1jL63PAmqUrvZNOfwAXNuaIwz4nvXYUuEPu59WRBCOIGDGRgv2wJ1O7O0xYXqDkI57EtAYf9KUK+miEn/Btg==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-axis": "^36.6.8",
+ "victory-core": "^36.6.8",
+ "victory-polar-axis": "^36.6.8",
+ "victory-shared-events": "^36.6.8"
+ }
+ },
+ "victory-core": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-core/-/victory-core-36.6.8.tgz",
+ "integrity": "sha512-SkyEszZKGyxjqfptfFWYdI22CvCuE9LhkaDpikzIhT2gcE3SuOBO5fk/740XMYE2ZUsJ4Fu/Vy4+8jZi17y44A==",
+ "requires": {
+ "lodash": "^4.17.21",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-vendor": "^36.6.8"
+ }
+ },
+ "victory-create-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-create-container/-/victory-create-container-36.6.8.tgz",
+ "integrity": "sha512-H2BsdTbJ/RxxcEg5lzk3TDlihtOs7I/5KaIBP3yosPs702i40mL2qndkRkj08QeiZhkaKfQ2GOUvyP+t7DSdmg==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "victory-brush-container": "^36.6.8",
+ "victory-core": "^36.6.8",
+ "victory-cursor-container": "^36.6.8",
+ "victory-selection-container": "^36.6.8",
+ "victory-voronoi-container": "^36.6.8",
+ "victory-zoom-container": "^36.6.8"
+ }
+ },
+ "victory-cursor-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-cursor-container/-/victory-cursor-container-36.6.8.tgz",
+ "integrity": "sha512-3WIBRl+7jnZok6syLfW8RK23nliDcoD/JUTN0YZo6bKBqHeFc4+ur3mlwCfghH7sGoxJRYuOJxTd9x2MwM5HQQ==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-errorbar": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-errorbar/-/victory-errorbar-36.6.8.tgz",
+ "integrity": "sha512-N4JdBy5wV+KU6pus7FBx+5on31oXanO+qVmtRH8u4W7CMWH5EwHortyu2wVYD9K2QoluXemIxZd7kfn14hmqfQ==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-group": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-group/-/victory-group-36.6.8.tgz",
+ "integrity": "sha512-CiupDIGPPWVgwif3ayd8glSlR41mVbuT0Nl0ay9q42w2fiM32syiJAoifIw47X4tL8ow/DXH+/5Pd8eEyA2trA==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8",
+ "victory-shared-events": "^36.6.8"
+ }
+ },
+ "victory-legend": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-legend/-/victory-legend-36.6.8.tgz",
+ "integrity": "sha512-OnkzB82Mvt5/1LYNsrfZQoXaVvgfp1rCsFRI3imq257Sh/UPy0/eZehCMQs/SVbU0z0EuIpXokhZb3BBdoJgpw==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-line": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-line/-/victory-line-36.6.8.tgz",
+ "integrity": "sha512-MozOejQRZPdzFaru5zUfqVB4TEff6nZjtQhOs+F5yyhXjLgM89zGX30r3jK5cjVdAPbTu4KPUrwktvlw+AkPRA==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8",
+ "victory-vendor": "^36.6.8"
+ }
+ },
+ "victory-pie": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-pie/-/victory-pie-36.6.8.tgz",
+ "integrity": "sha512-dUHWiiKd60dlt7OjFa+YYwanHAkP/T0abzy6O3SFxGre52oeqd8px1EoVhlLKpn4ao8L35koG9mvz6/pGyr8Dw==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8",
+ "victory-vendor": "^36.6.8"
+ }
+ },
+ "victory-polar-axis": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-polar-axis/-/victory-polar-axis-36.6.8.tgz",
+ "integrity": "sha512-aU+Wp5six21POhI9oXeREnZHljpqcmwFHHnliVGrwgRsuc7TAjfXPWVOX9guEFfh6zQW6IZWWWTTLAN/PIEm9w==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-scatter": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-scatter/-/victory-scatter-36.6.8.tgz",
+ "integrity": "sha512-GKSNneBxIWLsF3eBSTW5IwT5S4YdsfFl4PVCP3/wTa2myfS5DIS9FufEnJp/FEZGalEXYWxeR47rlWqABxAj5A==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-selection-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-selection-container/-/victory-selection-container-36.6.8.tgz",
+ "integrity": "sha512-kudYbSX+o7fr64oeN7+EG/c+lqO22aypxVdCwa6BagAGoqqLR4jXxTqqIdp8tvxCgfCCXxopnTKYr46nubypGw==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-shared-events": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-shared-events/-/victory-shared-events-36.6.8.tgz",
+ "integrity": "sha512-hWPOVqMD3Sv6Rl1iyO6ibQrwYF9/eLCnRo0T59/Hsid6On0AJJjL9gv0oEIM5fqz7R7zx9PJmMk877IctEOemw==",
+ "requires": {
+ "json-stringify-safe": "^5.0.1",
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-stack": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-stack/-/victory-stack-36.6.8.tgz",
+ "integrity": "sha512-Pkux46IqAealOi0KvqQpaJKKKpHCfZ/sh5IeUKYFy+QKWAjiQjG6hFZeHgr2YaS7OfdbvHhoAdvp03KntWzpbw==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8",
+ "victory-shared-events": "^36.6.8"
+ }
+ },
+ "victory-tooltip": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-tooltip/-/victory-tooltip-36.6.8.tgz",
+ "integrity": "sha512-9P+QeAGyDpP0trJnQ1NtnbDhpoJB0Ghc2boYEehvL12p0OzolY9/Nq5SDP0tu5i1BBujwFXtnoCDqt+mOH25fA==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "victory-vendor": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.8.tgz",
+ "integrity": "sha512-H3kyQ+2zgjMPvbPqAl7Vwm2FD5dU7/4bCTQakFQnpIsfDljeOMDojRsrmJfwh4oAlNnWhpAf+mbAoLh8u7dwyQ==",
+ "requires": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "victory-voronoi-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-voronoi-container/-/victory-voronoi-container-36.6.8.tgz",
+ "integrity": "sha512-x9/OOZdMm4dh38jNhSfBYT0nG6ribsINU0/WNzIn3QcDXFBInsJ7jRySxYmdmk45OdXfbDRwDMqVHk72sWQyUw==",
+ "requires": {
+ "delaunay-find": "0.0.6",
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "react-fast-compare": "^3.2.0",
+ "victory-core": "^36.6.8",
+ "victory-tooltip": "^36.6.8"
+ }
+ },
+ "victory-zoom-container": {
+ "version": "36.6.8",
+ "resolved": "https://registry.npmjs.org/victory-zoom-container/-/victory-zoom-container-36.6.8.tgz",
+ "integrity": "sha512-gxX5iJUaxrFFZ2IGS0sQnUI+3Mhj6bVLqtOlQd3Krld+9f/ieuUbxl+P+eIyhQU/VyHSlirIZeOGOXJeYcU9jQ==",
+ "requires": {
+ "lodash": "^4.17.19",
+ "prop-types": "^15.8.1",
+ "victory-core": "^36.6.8"
+ }
+ },
+ "warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "requires": {
+ "loose-envify": "^1.0.0"
+ }
+ },
+ "watchpack": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
+ "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+ "requires": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ }
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/package.json b/opendc-web/opendc-web-server/src/main/webui/package.json
new file mode 100644
index 00000000..8e5bda2a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "opendc-frontend",
+ "version": "3.0.0",
+ "description": "The user-facing component of the OpenDC stack, allowing users to build and interact with their own (virtual) datacenters.",
+ "keywords": [
+ "opendc",
+ "simulation",
+ "datacenter",
+ "frontend"
+ ],
+ "homepage": "http://opendc.org",
+ "bugs": {
+ "url": "https://github.com/atlarge-research/opendc/issues",
+ "email": "opendc@atlarge-research.com"
+ },
+ "author": "OpenDC Maintainers <opendc@atlarge-research.com>",
+ "license": "MIT",
+ "private": true,
+ "dependencies": {
+ "@auth0/auth0-react": "^1.12.1",
+ "@patternfly/react-charts": "^6.94.18",
+ "@patternfly/react-core": "^4.276.6",
+ "@patternfly/react-icons": "^4.93.6",
+ "@patternfly/react-table": "^4.112.39",
+ "@sentry/react": "^7.45.0",
+ "@sentry/tracing": "^7.45.0",
+ "clsx": "^1.2.1",
+ "immer": "^9.0.21",
+ "konva": "^8.4.3",
+ "mathjs": "^11.7.0",
+ "next": "^13.5.4",
+ "next-global-css": "^1.3.1",
+ "normalizr": "^3.6.2",
+ "prettier": "^2.8.7",
+ "prop-types": "^15.8.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-hotkeys-hook": "^4.3.8",
+ "react-konva": "^18.2.5",
+ "react-query": "^3.39.3",
+ "react-redux": "^8.0.5",
+ "next-runtime-env": "^1.7.1",
+ "redux": "^4.2.1",
+ "redux-logger": "^3.0.6",
+ "redux-saga": "^1.2.3",
+ "redux-thunk": "^2.4.2",
+ "svgsaver": "^0.9.0",
+ "use-resize-observer": "^9.1.0",
+ "uuid": "^9.0.0",
+ "victory-errorbar": "^36.6.8"
+ },
+ "devDependencies": {
+ "eslint": "^8.36.0",
+ "eslint-config-next": "^13.2.4"
+ },
+ "scripts": {
+ "format": "prettier --write src",
+ "precommit": "lint-staged",
+ "dev": "next dev",
+ "lint": "next lint",
+ "build": "next build && next export",
+ "start": "next start",
+ "export": "next export -o build/export"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/pages/404.js b/opendc-web/opendc-web-server/src/main/webui/pages/404.js
new file mode 100644
index 00000000..0939bc56
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/pages/404.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import Head from 'next/head'
+import { AppPage } from '../components/AppPage'
+import {
+ Bullseye,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ PageSection,
+ PageSectionVariants,
+ Title,
+} from '@patternfly/react-core'
+import { UnknownIcon } from '@patternfly/react-icons'
+
+const NotFound = () => {
+ return (
+ <AppPage>
+ <Head>
+ <title>Page Not Found - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={UnknownIcon} />
+ <Title size="lg" headingLevel="h4">
+ 404: That page does not exist
+ </Title>
+ <EmptyStateBody>
+ The requested page is not found. Try refreshing the page if it was recently added.
+ </EmptyStateBody>
+ </EmptyState>
+ </Bullseye>
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default NotFound
diff --git a/opendc-web/opendc-web-server/src/main/webui/pages/_app.js b/opendc-web/opendc-web-server/src/main/webui/pages/_app.js
new file mode 100644
index 00000000..62ce0539
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/pages/_app.js
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import Head from 'next/head'
+import Script from 'next/script'
+import { Provider } from 'react-redux'
+import { useNewQueryClient } from '../data/query'
+import { useStore } from '../redux'
+import { AuthProvider, useRequireAuth } from '../auth'
+import * as Sentry from '@sentry/react'
+import { Integrations } from '@sentry/tracing'
+import { QueryClientProvider } from 'react-query'
+import { sentryDsn } from '../config'
+
+import '@patternfly/react-core/dist/styles/base.css'
+import '@patternfly/react-styles/css/utilities/Alignment/alignment.css'
+import '@patternfly/react-styles/css/utilities/BackgroundColor/BackgroundColor.css'
+import '@patternfly/react-styles/css/utilities/BoxShadow/box-shadow.css'
+import '@patternfly/react-styles/css/utilities/Display/display.css'
+import '@patternfly/react-styles/css/utilities/Flex/flex.css'
+import '@patternfly/react-styles/css/utilities/Float/float.css'
+import '@patternfly/react-styles/css/utilities/Sizing/sizing.css'
+import '@patternfly/react-styles/css/utilities/Spacing/spacing.css'
+import '@patternfly/react-styles/css/utilities/Text/text.css'
+import '@patternfly/react-styles/css/components/InlineEdit/inline-edit.css'
+import '../style/index.css'
+
+// This setup is necessary to forward the Auth0 context to the Redux context
+function Inner({ Component, pageProps }) {
+ // Force user to be authorized
+ useRequireAuth()
+
+ const queryClient = useNewQueryClient()
+ const store = useStore(pageProps.initialReduxState, { queryClient })
+ return (
+ <QueryClientProvider client={queryClient}>
+ <Provider store={store}>
+ <Component {...pageProps} />
+ </Provider>
+ </QueryClientProvider>
+ )
+}
+
+Inner.propTypes = {
+ Component: PropTypes.func,
+ pageProps: PropTypes.shape({
+ initialReduxState: PropTypes.object,
+ }).isRequired,
+}
+
+// Initialize Sentry if the user has configured a DSN
+if (process.browser && sentryDsn) {
+ Sentry.init({
+ environment: process.env.NODE_ENV,
+ dsn: sentryDsn,
+ integrations: [new Integrations.BrowserTracing()],
+ tracesSampleRate: 0.1,
+ })
+}
+
+export default function App(props) {
+ return (
+ <>
+ <Head>
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
+ <meta name="theme-color" content="#00A6D6" />
+ </Head>
+ <Sentry.ErrorBoundary fallback={'An error has occurred'}>
+ <AuthProvider>
+ <Inner {...props} />
+ </AuthProvider>
+ </Sentry.ErrorBoundary>
+ {/* Google Analytics */}
+ <Script async src="https://www.googletagmanager.com/gtag/js?id=UA-84285092-3" />
+ <Script
+ id="gtag"
+ dangerouslySetInnerHTML={{
+ __html: `
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'UA-84285092-3');
+ `,
+ }}
+ />
+ </>
+ )
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/pages/_document.js b/opendc-web/opendc-web-server/src/main/webui/pages/_document.js
new file mode 100644
index 00000000..0a01a3eb
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/pages/_document.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import Document, { Html, Head, Main, NextScript } from 'next/document'
+
+class OpenDCDocument extends Document {
+ render() {
+ return (
+ <Html lang="en">
+ <Head>
+ <meta charSet="utf-8" />
+ <meta name="theme-color" content="#00A6D6" />
+ <meta
+ name="description"
+ content="Collaborative Datacenter Simulation and Exploration for Everybody"
+ />
+ <meta name="author" content="@Large Research" />
+ <meta
+ name="keywords"
+ content="OpenDC, Datacenter, Simulation, Simulator, Collaborative, Distributed, Cluster"
+ />
+ <link rel="manifest" href="/manifest.json" />
+ <link rel="shortcut icon" href="/favicon.ico" />
+
+ {/* Twitter Card data */}
+ <meta name="twitter:card" content="summary" />
+ <meta name="twitter:site" content="@LargeResearch" />
+ <meta name="twitter:title" content="OpenDC" />
+ <meta
+ name="twitter:description"
+ content="Collaborative Datacenter Simulation and Exploration for Everybody"
+ />
+ <meta name="twitter:creator" content="@LargeResearch" />
+ <meta name="twitter:image" content="http://opendc.org/img/logo.png" />
+
+ {/* OpenGraph meta tags */}
+ <meta property="og:title" content="OpenDC" />
+ <meta property="og:site_name" content="OpenDC" />
+ <meta property="og:type" content="website" />
+ <meta property="og:image" content="http://opendc.org/img/logo.png" />
+ <meta property="og:url" content="http://opendc.org/" />
+ <meta
+ property="og:description"
+ content="OpenDC provides collaborative online datacenter modeling, diverse and effective datacenter simulation, and exploratory datacenter performance feedback."
+ />
+ <meta property="og:locale" content="en_US" />
+ {/* Load config before Next scripts */}
+ <script src="/__ENV.js" />
+ </Head>
+ <body>
+ <Main />
+ <NextScript />
+ </body>
+ </Html>
+ )
+ }
+}
+
+export default OpenDCDocument
diff --git a/opendc-web/opendc-web-server/src/main/webui/pages/logout.js b/opendc-web/opendc-web-server/src/main/webui/pages/logout.js
new file mode 100644
index 00000000..38d5968e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/pages/logout.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import Head from 'next/head'
+import { AppPage } from '../components/AppPage'
+import { PageSection, PageSectionVariants } from '@patternfly/react-core'
+
+function Logout() {
+ return (
+ <AppPage>
+ <Head>
+ <title>Logged Out - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>Logged out successfully</PageSection>
+ </AppPage>
+ )
+}
+
+export default Logout
diff --git a/opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/index.js b/opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/index.js
new file mode 100644
index 00000000..52938bcd
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/index.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useRouter } from 'next/router'
+import ProjectOverview from '../../../components/projects/ProjectOverview'
+import { useProject } from '../../../data/project'
+import { AppPage } from '../../../components/AppPage'
+import Head from 'next/head'
+import Link from 'next/link'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ PageSection,
+ PageSectionVariants,
+ Skeleton,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+
+function Project() {
+ const router = useRouter()
+ const projectId = +router.query['project']
+
+ const { data: project } = useProject(+projectId)
+
+ const breadcrumb = (
+ <Breadcrumb>
+ <BreadcrumbItem to="/projects" component={Link}>
+ Projects
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}`} component={Link} isActive>
+ Project details
+ </BreadcrumbItem>
+ </Breadcrumb>
+ )
+
+ return (
+ <AppPage breadcrumb={breadcrumb}>
+ <Head>
+ <title>{`${project?.name ?? 'Project'} - OpenDC`}</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">
+ {project?.name ?? <Skeleton width="15%" screenreaderText="Loading project" />}
+ </Text>
+ </TextContent>
+ </PageSection>
+ <PageSection isFilled>
+ <ProjectOverview projectId={projectId} />
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default Project
diff --git a/opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/portfolios/[portfolio].js b/opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/portfolios/[portfolio].js
new file mode 100644
index 00000000..5d1e041b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/portfolios/[portfolio].js
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import dynamic from 'next/dynamic'
+import { useRouter } from 'next/router'
+import Head from 'next/head'
+import Link from 'next/link'
+import React, { useRef } from 'react'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Divider,
+ PageSection,
+ PageSectionVariants,
+ Tab,
+ TabContent,
+ Tabs,
+ TabTitleText,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import { AppPage } from '../../../../components/AppPage'
+import ContextSelectionSection from '../../../../components/context/ContextSelectionSection'
+import PortfolioSelector from '../../../../components/context/PortfolioSelector'
+import PortfolioOverview from '../../../../components/portfolios/PortfolioOverview'
+import { usePortfolio } from '../../../../data/project'
+
+const PortfolioResults = dynamic(() => import('../../../../components/portfolios/PortfolioResults'), { ssr: false })
+
+/**
+ * Page that displays the results in a portfolio.
+ */
+function Portfolio() {
+ const router = useRouter()
+ const projectId = +router.query['project']
+ const portfolioNumber = +router.query['portfolio']
+
+ const overviewRef = useRef(null)
+ const resultsRef = useRef(null)
+
+ const { data: portfolio } = usePortfolio(projectId, portfolioNumber)
+
+ const breadcrumb = (
+ <Breadcrumb>
+ <BreadcrumbItem to="/projects" component={Link}>
+ Projects
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}`} component={Link}>
+ Project details
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}/portfolios/${portfolioNumber}`} component={Link} isActive>
+ Portfolio
+ </BreadcrumbItem>
+ </Breadcrumb>
+ )
+
+ const contextSelectors = (
+ <ContextSelectionSection>
+ <PortfolioSelector activePortfolio={portfolio} />
+ </ContextSelectionSection>
+ )
+
+ return (
+ <AppPage breadcrumb={breadcrumb} contextSelectors={contextSelectors}>
+ <Head>
+ <title>Portfolio - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">Portfolio</Text>
+ </TextContent>
+ </PageSection>
+ <PageSection type="tabs" variant={PageSectionVariants.light} stickyOnBreakpoint={{ default: 'top' }}>
+ <Divider component="div" />
+ <Tabs defaultActiveKey={0} className="pf-m-page-insets">
+ <Tab
+ eventKey={0}
+ title={<TabTitleText>Overview</TabTitleText>}
+ tabContentId="overview"
+ tabContentRef={overviewRef}
+ />
+ <Tab
+ eventKey={1}
+ title={<TabTitleText>Results</TabTitleText>}
+ tabContentId="results"
+ tabContentRef={resultsRef}
+ />
+ </Tabs>
+ </PageSection>
+ <PageSection isFilled>
+ <TabContent eventKey={0} id="overview" ref={overviewRef} aria-label="Overview tab">
+ <PortfolioOverview projectId={projectId} portfolioId={portfolioNumber} />
+ </TabContent>
+ <TabContent eventKey={1} id="results" ref={resultsRef} aria-label="Results tab" hidden>
+ <PortfolioResults projectId={projectId} portfolioId={portfolioNumber} />
+ </TabContent>
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default Portfolio
diff --git a/opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/topologies/[topology].js b/opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/topologies/[topology].js
new file mode 100644
index 00000000..48359365
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/pages/projects/[project]/topologies/[topology].js
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import dynamic from 'next/dynamic'
+import { useRouter } from 'next/router'
+import Head from 'next/head'
+import Link from 'next/link'
+import ContextSelectionSection from '../../../../components/context/ContextSelectionSection'
+import TopologySelector from '../../../../components/context/TopologySelector'
+import TopologyOverview from '../../../../components/topologies/TopologyOverview'
+import { useDispatch } from 'react-redux'
+import React, { useEffect, useState } from 'react'
+import { AppPage } from '../../../../components/AppPage'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Divider,
+ PageSection,
+ PageSectionVariants,
+ Tab,
+ TabContent,
+ Tabs,
+ TabTitleText,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import { useTopology } from '../../../../data/topology'
+import { goToRoom } from '../../../../redux/actions/interaction-level'
+import { openTopology } from '../../../../redux/actions/topology'
+
+const TopologyMap = dynamic(() => import('../../../../components/topologies/TopologyMap'), { ssr: false })
+
+/**
+ * Page that displays a datacenter topology.
+ */
+function Topology() {
+ const router = useRouter()
+ const projectId = +router.query['project']
+ const topologyNumber = +router.query['topology']
+
+ const { data: topology } = useTopology(projectId, topologyNumber)
+
+ const dispatch = useDispatch()
+ useEffect(() => {
+ if (topologyNumber) {
+ dispatch(openTopology(projectId, topologyNumber))
+ }
+ }, [projectId, topologyNumber, dispatch])
+
+ const [activeTab, setActiveTab] = useState('overview')
+
+ const breadcrumb = (
+ <Breadcrumb>
+ <BreadcrumbItem to="/projects" component={Link}>
+ Projects
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}`} component={Link}>
+ Project details
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}/topologies/${topologyNumber}`} component={Link} isActive>
+ Topology
+ </BreadcrumbItem>
+ </Breadcrumb>
+ )
+
+ const contextSelectors = (
+ <ContextSelectionSection>
+ <TopologySelector activeTopology={topology} />
+ </ContextSelectionSection>
+ )
+
+ return (
+ <AppPage breadcrumb={breadcrumb} contextSelectors={contextSelectors}>
+ <Head>
+ <title>{`${topology?.name ?? 'Topologies'} - OpenDC`}</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">Topology</Text>
+ </TextContent>
+ </PageSection>
+ <PageSection type="none" variant={PageSectionVariants.light} className="pf-c-page__main-tabs" sticky="top">
+ <Divider component="div" />
+ <Tabs
+ activeKey={activeTab}
+ onSelect={(_, tabIndex) => setActiveTab(tabIndex)}
+ className="pf-m-page-insets"
+ >
+ <Tab eventKey="overview" title={<TabTitleText>Overview</TabTitleText>} tabContentId="overview" />
+ <Tab
+ eventKey="floor-plan"
+ title={<TabTitleText>Floor Plan</TabTitleText>}
+ tabContentId="floor-plan"
+ />
+ </Tabs>
+ </PageSection>
+ <PageSection padding={activeTab === 'floor-plan' && { default: 'noPadding' }} isFilled>
+ <TabContent id="overview" aria-label="Overview tab" hidden={activeTab !== 'overview'}>
+ <TopologyOverview
+ projectId={projectId}
+ topologyNumber={topologyNumber}
+ onSelect={(type, obj) => {
+ if (type === 'room') {
+ dispatch(goToRoom(obj.id))
+ setActiveTab('floor-plan')
+ }
+ }}
+ />
+ </TabContent>
+ <TabContent
+ id="floor-plan"
+ aria-label="Floor Plan tab"
+ className="pf-u-h-100"
+ hidden={activeTab !== 'floor-plan'}
+ >
+ <TopologyMap />
+ </TabContent>
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default Topology
diff --git a/opendc-web/opendc-web-server/src/main/webui/pages/projects/index.js b/opendc-web/opendc-web-server/src/main/webui/pages/projects/index.js
new file mode 100644
index 00000000..97ff105c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/pages/projects/index.js
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { PlusIcon } from '@patternfly/react-icons'
+import React, { useMemo, useState } from 'react'
+import Head from 'next/head'
+import ProjectFilterPanel from '../../components/projects/FilterPanel'
+import { AppPage } from '../../components/AppPage'
+import {
+ PageSection,
+ PageSectionVariants,
+ Title,
+ Toolbar,
+ ToolbarItem,
+ ToolbarContent,
+ Button,
+ TextContent,
+ Text,
+} from '@patternfly/react-core'
+import ProjectCollection from '../../components/projects/ProjectCollection'
+import TextInputModal from '../../components/util/modals/TextInputModal'
+import { useProjects, useDeleteProject, useNewProject } from '../../data/project'
+
+const getVisibleProjects = (projects, filter) => {
+ switch (filter) {
+ case 'SHOW_ALL':
+ return projects
+ case 'SHOW_OWN':
+ return projects.filter((project) => project.role === 'OWNER')
+ case 'SHOW_SHARED':
+ return projects.filter((project) => project.role !== 'OWNER')
+ default:
+ return projects
+ }
+}
+
+function Projects() {
+ const { status, data: projects } = useProjects()
+ const [filter, setFilter] = useState('SHOW_ALL')
+ const visibleProjects = useMemo(() => getVisibleProjects(projects ?? [], filter), [projects, filter])
+
+ const { mutate: deleteProject } = useDeleteProject()
+ const { mutate: addProject } = useNewProject()
+
+ const [isProjectCreationModalVisible, setProjectCreationModalVisible] = useState(false)
+ const onProjectCreation = (name) => {
+ if (name) {
+ addProject({ name })
+ }
+ setProjectCreationModalVisible(false)
+ }
+
+ return (
+ <AppPage>
+ <Head>
+ <title>My Projects - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light} isFilled>
+ <div className="pf-u-mx-auto pf-u-max-width" style={{ '--pf-u-max-width--MaxWidth': '100ch' }}>
+ <Title className="pf-u-mt-xl-on-md" headingLevel="h1" size="4xl">
+ Welcome
+ </Title>
+ <TextContent>
+ <Text component="p">Find all your personal and shared projects</Text>
+ </TextContent>
+ <Toolbar inset={{ default: 'insetNone' }}>
+ <ToolbarContent>
+ <ToolbarItem>
+ <ProjectFilterPanel onSelect={setFilter} activeFilter={filter} />
+ </ToolbarItem>
+ <ToolbarItem alignment={{ default: 'alignRight' }}>
+ <Button icon={<PlusIcon />} onClick={() => setProjectCreationModalVisible(true)}>
+ Create Project
+ </Button>
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ <ProjectCollection
+ status={status}
+ isFiltering={filter !== 'SHOW_ALL'}
+ projects={visibleProjects}
+ onDelete={(project) => deleteProject(project.id)}
+ onCreate={() => setProjectCreationModalVisible(true)}
+ />
+ <TextInputModal
+ title="New Project"
+ label="Project name"
+ isOpen={isProjectCreationModalVisible}
+ callback={onProjectCreation}
+ />
+ </div>
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default Projects
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/favicon.ico b/opendc-web/opendc-web-server/src/main/webui/public/favicon.ico
new file mode 100644
index 00000000..c2f40a0d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/favicon.ico
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/humans.txt b/opendc-web/opendc-web-server/src/main/webui/public/humans.txt
new file mode 100644
index 00000000..dadcd530
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/humans.txt
@@ -0,0 +1,35 @@
+/* TEAM */
+Benevolent Dictator for Life: Alexandru Iosup.
+Site: http://www.ds.ewi.tudelft.nl/~iosup/
+Twitter: aiosup.
+Location: Delft, Netherlands.
+
+Full-Stack Engineer: Georgios Andreadis.
+Site: https://github.com/gandreadis
+Location: Delft, Netherlands.
+
+Simulation Engineer: Fabian Mastenbroek.
+Site: https://github.com/fabianishere
+Location: Delft, Netherlands.
+
+Simulation Engineer: Jacob Burley.
+Site: https://github.com/jc0b
+Location: Amsterdam, Netherlands.
+
+Backend Engineer: Leon Overweel.
+Site: http://leonoverweel.com/
+Twitter: layon_overwhale.
+Location: Delft, Netherlands.
+
+Simulation Engineer: Matthijs Bijman.
+Site: https://github.com/MDBijman
+Location: Delft, Netherlands.
+
+/* THANKS */
+Executive Producer: Vincent van Beek.
+Executive Producer: Tim Hegeman.
+
+/* SITE */
+Standards: HTML5, Sass, ES6
+Components: React.js, Redux, create-react-app, react-konva
+Software: WebStorm, Vim, Visual Studio
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/avatar.svg b/opendc-web/opendc-web-server/src/main/webui/public/img/avatar.svg
new file mode 100644
index 00000000..73726f9b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/avatar.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 36 36" style="enable-background:new 0 0 36 36;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill-rule:evenodd;clip-rule:evenodd;fill:#F0F0F0;}
+ .st1{fill-rule:evenodd;clip-rule:evenodd;fill:#D2D2D2;}
+ .st2{fill:#B8BBBE;}
+ .st3{fill:#D2D2D2;}
+</style>
+<rect class="st0" width="36" height="36"/>
+<path class="st1" d="M17.7,20.1c-3.5,0-6.4-2.9-6.4-6.4s2.9-6.4,6.4-6.4s6.4,2.9,6.4,6.4S21.3,20.1,17.7,20.1z"/>
+<path class="st2" d="M13.3,36l0-6.7c-2,0.4-2.9,1.4-3.1,3.5L10.1,36H13.3z"/>
+<path class="st3" d="M10.1,36l0.1-3.2c0.2-2.1,1.1-3.1,3.1-3.5l0,6.7h9.4l0-6.7c2,0.4,2.9,1.4,3.1,3.5l0.1,3.2h4.7
+ c-0.4-3.9-1.3-9-2.9-11c-1.1-1.4-2.3-2.2-3.5-2.6s-1.8-0.6-6.3-0.6s-6.1,0.7-6.1,0.7c-1.2,0.4-2.4,1.2-3.4,2.6
+ C6.7,27,5.8,32.2,5.4,36H10.1z"/>
+<path class="st2" d="M25.9,36l-0.1-3.2c-0.2-2.1-1.1-3.1-3.1-3.5l0,6.7H25.9z"/>
+</svg>
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/datacenter-drawing.png b/opendc-web/opendc-web-server/src/main/webui/public/img/datacenter-drawing.png
new file mode 100644
index 00000000..ec2b7398
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/datacenter-drawing.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/logo.png b/opendc-web/opendc-web-server/src/main/webui/public/img/logo.png
new file mode 100644
index 00000000..d743038b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/logo.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/logo.svg b/opendc-web/opendc-web-server/src/main/webui/public/img/logo.svg
new file mode 100644
index 00000000..5283a034
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/logo.svg
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="4.3656249mm"
+ height="5.4239674mm"
+ viewBox="0 0 4.3656249 5.4239674"
+ version="1.1"
+ id="svg4738"
+ inkscape:version="0.92.1 r15371"
+ sodipodi:docname="opendc.svg">
+ <defs
+ id="defs4732" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="22.4"
+ inkscape:cx="-5.0874286"
+ inkscape:cy="6.401307"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1027"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata4735">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-16.538532,-146.52225)">
+ <g
+ transform="matrix(0.26458333,0,0,0.26458333,-15.13354,-112.7517)"
+ id="g4333"
+ inkscape:export-filename="h:\Desktop\logo.png"
+ inkscape:export-xdpi="561.54816"
+ inkscape:export-ydpi="561.54816">
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\stackv2.png"
+ style="fill:#48a1cd;fill-opacity:1;stroke:none"
+ d="m 119.95547,988.18306 8,4 v -4 l -8,-4 z"
+ id="path4052-1-2-8"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\stackv2.png"
+ style="fill:#82d0e7;fill-opacity:1;stroke:none"
+ d="m 127.95547,988.18306 v 4 l 8.00001,-4 v -4 z"
+ id="path4054-4-9-2"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\stackv2.png"
+ style="fill:#82d0e7;fill-opacity:1;stroke:none"
+ d="m 119.95547,984.18306 8,-4 8.00001,4 -8.00001,4 z"
+ id="path4056-2-1-5"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\comparison.png"
+ inkscape:connector-curvature="0"
+ id="path4048-1-9"
+ d="m 119.95547,992.18306 8,4 v -4 l -8,-4 z"
+ style="fill:#d1af2e;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\comparison.png"
+ inkscape:connector-curvature="0"
+ id="path4050-7-2"
+ d="m 127.95546,992.18306 v 4 l 8.00001,-4 v -4 z"
+ style="fill:#edd667;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\comparison.png"
+ style="fill:#df6f20;fill-opacity:1;stroke:none"
+ d="m 119.95547,996.18306 8,4.00004 v -4.00004 l -8,-4 z"
+ id="path3883-9-9-8"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\comparison.png"
+ style="fill:#ed9c67;fill-opacity:1;stroke:none"
+ d="m 127.95547,996.18306 v 4.00004 l 8,-4.00004 v -4 z"
+ id="path3885-8-9-1"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\comparison.png"
+ style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+ d="m 119.95547,984.18306 8,-4 8,4 -8,4 z"
+ id="path3893-6-0-8"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\comparison.png"
+ style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 119.95547,984.18306 v 4 l 8,4 8,-4 v -4"
+ id="path3895-2-6-1"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\comparison.png"
+ style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 127.95547,988.18306 v 12.00004"
+ id="path3897-3-2-1"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\comparison.png"
+ inkscape:connector-curvature="0"
+ id="path3899-8-1-7"
+ d="m 119.95547,988.18306 v 4 l 8,4 8,-4 v -4"
+ style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\comparison.png"
+ style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+ d="m 119.95547,992.18306 v 4 l 8,4.00004 8,-4.00004 v -4"
+ id="path3901-5-6-1"
+ inkscape:connector-curvature="0" />
+ <circle
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\stackv2.png"
+ transform="matrix(1.0403949,-0.44307824,0.3060712,0.9369482,-235.62413,33.696703)"
+ id="path3906-6-2-4-1-2"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ cx="43.5"
+ cy="1044.8622"
+ r="0.5" />
+ <circle
+ inkscape:export-ydpi="1307.2168"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-filename="h:\Desktop\stackv2.png"
+ transform="matrix(1.0403949,-0.44307824,0.3060712,0.9369482,-235.62413,37.696663)"
+ id="path3910-3-0-1-9-1"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ cx="43.5"
+ cy="1044.8622"
+ r="0.5" />
+ <circle
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ id="path4108-7-3-6"
+ transform="matrix(1.0403949,-0.44307824,0.3060712,0.9369482,-235.62413,29.696659)"
+ inkscape:export-filename="h:\Desktop\stackv2.png"
+ inkscape:export-xdpi="1307.2168"
+ inkscape:export-ydpi="1307.2168"
+ cx="43.5"
+ cy="1044.8622"
+ r="0.5" />
+ </g>
+ </g>
+</svg>
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/opendc-architecture.png b/opendc-web/opendc-web-server/src/main/webui/public/img/opendc-architecture.png
new file mode 100644
index 00000000..e0bf8e9b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/opendc-architecture.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/opendc-timeline-v2.png b/opendc-web/opendc-web-server/src/main/webui/public/img/opendc-timeline-v2.png
new file mode 100644
index 00000000..0b2821c4
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/opendc-timeline-v2.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/aiosup.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/aiosup.png
new file mode 100644
index 00000000..d2019b4d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/aiosup.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/evaneyk.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/evaneyk.png
new file mode 100644
index 00000000..011c1627
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/evaneyk.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/fmastenbroek.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/fmastenbroek.png
new file mode 100644
index 00000000..218b1a6f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/fmastenbroek.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/gandreadis.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/gandreadis.png
new file mode 100644
index 00000000..96a3abda
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/gandreadis.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/hhe.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/hhe.png
new file mode 100644
index 00000000..4891c7f5
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/hhe.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jbosch.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jbosch.png
new file mode 100644
index 00000000..c76e1fab
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jbosch.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jburley.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jburley.png
new file mode 100644
index 00000000..d2691659
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jburley.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/lfdversluis.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/lfdversluis.png
new file mode 100644
index 00000000..6fbc8472
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/lfdversluis.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/loverweel.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/loverweel.png
new file mode 100644
index 00000000..85865977
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/loverweel.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/sjounaid.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/sjounaid.png
new file mode 100644
index 00000000..41878161
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/sjounaid.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/vvanbeek.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/vvanbeek.png
new file mode 100644
index 00000000..4c8b3311
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/vvanbeek.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/wlai.png b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/wlai.png
new file mode 100644
index 00000000..c758846d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/wlai.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-construction.png b/opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-construction.png
new file mode 100644
index 00000000..ea20a7c4
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-construction.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-simulation.png b/opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-simulation.png
new file mode 100644
index 00000000..1bd989c7
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-simulation.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Developer.png b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Developer.png
new file mode 100644
index 00000000..d2638e6c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Developer.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Manager.png b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Manager.png
new file mode 100644
index 00000000..92db7459
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Manager.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Researcher.png b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Researcher.png
new file mode 100644
index 00000000..d87edd39
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Researcher.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Sales.png b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Sales.png
new file mode 100644
index 00000000..5b7c3a72
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Sales.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Student.png b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Student.png
new file mode 100644
index 00000000..a4900303
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Student.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/topology/cpu-icon.png b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/cpu-icon.png
new file mode 100644
index 00000000..07cfbd31
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/cpu-icon.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/topology/gpu-icon.png b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/gpu-icon.png
new file mode 100644
index 00000000..55d4fb05
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/gpu-icon.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/topology/memory-icon.png b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/memory-icon.png
new file mode 100644
index 00000000..36e8a44e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/memory-icon.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-energy-icon.png b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-energy-icon.png
new file mode 100644
index 00000000..1088c61b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-energy-icon.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-space-icon.png b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-space-icon.png
new file mode 100644
index 00000000..387d7ea6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-space-icon.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/topology/storage-icon.png b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/storage-icon.png
new file mode 100644
index 00000000..7a39cb6f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/storage-icon.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/img/tudelft-icon.png b/opendc-web/opendc-web-server/src/main/webui/public/img/tudelft-icon.png
new file mode 100644
index 00000000..a7a2d56a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/img/tudelft-icon.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/manifest.json b/opendc-web/opendc-web-server/src/main/webui/public/manifest.json
new file mode 100644
index 00000000..adb82218
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "OpenDC",
+ "name": "OpenDC",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "16x16",
+ "type": "image/png"
+ }
+ ],
+ "start_url": "./index.html",
+ "display": "standalone",
+ "theme_color": "#00A6D6",
+ "background_color": "#eeeeee"
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/public/robots.txt b/opendc-web/opendc-web-server/src/main/webui/public/robots.txt
new file mode 100644
index 00000000..1c6094ce
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/public/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Disallow: /projects/
+Disallow: /profile/
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/interaction-level.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/interaction-level.js
new file mode 100644
index 00000000..8381eeef
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/interaction-level.js
@@ -0,0 +1,57 @@
+export const GO_FROM_BUILDING_TO_ROOM = 'GO_FROM_BUILDING_TO_ROOM'
+export const GO_FROM_ROOM_TO_RACK = 'GO_FROM_ROOM_TO_RACK'
+export const GO_FROM_RACK_TO_MACHINE = 'GO_FROM_RACK_TO_MACHINE'
+export const GO_DOWN_ONE_INTERACTION_LEVEL = 'GO_DOWN_ONE_INTERACTION_LEVEL'
+
+export function goToRoom(roomId) {
+ return {
+ type: GO_FROM_BUILDING_TO_ROOM,
+ roomId,
+ }
+}
+
+export function goFromBuildingToRoom(roomId) {
+ return (dispatch, getState) => {
+ const { interactionLevel } = getState()
+ if (interactionLevel.mode !== 'BUILDING') {
+ return
+ }
+
+ dispatch({
+ type: GO_FROM_BUILDING_TO_ROOM,
+ roomId,
+ })
+ }
+}
+
+export function goFromRoomToRack(tileId) {
+ return (dispatch, getState) => {
+ const { interactionLevel } = getState()
+ if (interactionLevel.mode !== 'ROOM') {
+ return
+ }
+ dispatch({
+ type: GO_FROM_ROOM_TO_RACK,
+ tileId,
+ })
+ }
+}
+
+export function goFromRackToMachine(position) {
+ return (dispatch, getState) => {
+ const { interactionLevel } = getState()
+ if (interactionLevel.mode !== 'RACK') {
+ return
+ }
+ dispatch({
+ type: GO_FROM_RACK_TO_MACHINE,
+ position,
+ })
+ }
+}
+
+export function goDownOneInteractionLevel() {
+ return {
+ type: GO_DOWN_ONE_INTERACTION_LEVEL,
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/building.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/building.js
new file mode 100644
index 00000000..c12417b9
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/building.js
@@ -0,0 +1,113 @@
+import { v4 as uuid } from 'uuid'
+import { addRoom, deleteRoom } from './room'
+
+export const START_NEW_ROOM_CONSTRUCTION = 'START_NEW_ROOM_CONSTRUCTION'
+export const START_NEW_ROOM_CONSTRUCTION_SUCCEEDED = 'START_NEW_ROOM_CONSTRUCTION_SUCCEEDED'
+export const FINISH_NEW_ROOM_CONSTRUCTION = 'FINISH_NEW_ROOM_CONSTRUCTION'
+export const CANCEL_NEW_ROOM_CONSTRUCTION = 'CANCEL_NEW_ROOM_CONSTRUCTION'
+export const CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED = 'CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED'
+export const START_ROOM_EDIT = 'START_ROOM_EDIT'
+export const FINISH_ROOM_EDIT = 'FINISH_ROOM_EDIT'
+export const ADD_TILE = 'ADD_TILE'
+export const DELETE_TILE = 'DELETE_TILE'
+
+export function startNewRoomConstruction() {
+ return (dispatch, getState) => {
+ const { topology } = getState()
+ const topologyId = topology.root.id
+ const room = {
+ id: uuid(),
+ name: 'Room',
+ topologyId,
+ tiles: [],
+ }
+
+ dispatch(addRoom(topologyId, room))
+ dispatch(startNewRoomConstructionSucceeded(room.id))
+ }
+}
+
+export function startNewRoomConstructionSucceeded(roomId) {
+ return {
+ type: START_NEW_ROOM_CONSTRUCTION_SUCCEEDED,
+ roomId,
+ }
+}
+
+export function finishNewRoomConstruction() {
+ return (dispatch, getState) => {
+ const { topology, construction } = getState()
+ if (topology.rooms[construction.currentRoomInConstruction].tiles.length === 0) {
+ dispatch(cancelNewRoomConstruction())
+ return
+ }
+
+ dispatch({
+ type: FINISH_NEW_ROOM_CONSTRUCTION,
+ })
+ }
+}
+
+export function cancelNewRoomConstruction() {
+ return (dispatch, getState) => {
+ const { construction } = getState()
+ const roomId = construction.currentRoomInConstruction
+ dispatch(deleteRoom(roomId))
+ dispatch(cancelNewRoomConstructionSucceeded())
+ }
+}
+
+export function cancelNewRoomConstructionSucceeded() {
+ return {
+ type: CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED,
+ }
+}
+
+export function startRoomEdit(roomId) {
+ return {
+ type: START_ROOM_EDIT,
+ roomId: roomId,
+ }
+}
+
+export function finishRoomEdit() {
+ return {
+ type: FINISH_ROOM_EDIT,
+ }
+}
+
+export function toggleTileAtLocation(positionX, positionY) {
+ return (dispatch, getState) => {
+ const { topology, construction } = getState()
+
+ const roomId = construction.currentRoomInConstruction
+ const tileIds = topology.rooms[roomId].tiles
+ for (const tileId of tileIds) {
+ if (topology.tiles[tileId].positionX === positionX && topology.tiles[tileId].positionY === positionY) {
+ dispatch(deleteTile(tileId))
+ return
+ }
+ }
+
+ dispatch(addTile(roomId, positionX, positionY))
+ }
+}
+
+export function addTile(roomId, positionX, positionY) {
+ return {
+ type: ADD_TILE,
+ tile: {
+ id: uuid(),
+ roomId,
+ positionX,
+ positionY,
+ },
+ }
+}
+
+export function deleteTile(tileId) {
+ return {
+ type: DELETE_TILE,
+ tileId,
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/index.js
new file mode 100644
index 00000000..d48af37a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/index.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+export const OPEN_TOPOLOGY = 'OPEN_TOPOLOGY'
+export const STORE_TOPOLOGY = 'STORE_TOPOLOGY'
+
+export function openTopology(projectId, id) {
+ return {
+ type: OPEN_TOPOLOGY,
+ projectId,
+ id,
+ }
+}
+
+export function storeTopology(topology, entities) {
+ return {
+ type: STORE_TOPOLOGY,
+ topology,
+ entities,
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/machine.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/machine.js
new file mode 100644
index 00000000..93320884
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/machine.js
@@ -0,0 +1,28 @@
+export const DELETE_MACHINE = 'DELETE_MACHINE'
+export const ADD_UNIT = 'ADD_UNIT'
+export const DELETE_UNIT = 'DELETE_UNIT'
+
+export function deleteMachine(machineId) {
+ return {
+ type: DELETE_MACHINE,
+ machineId,
+ }
+}
+
+export function addUnit(machineId, unitType, unitId) {
+ return {
+ type: ADD_UNIT,
+ machineId,
+ unitType,
+ unitId,
+ }
+}
+
+export function deleteUnit(machineId, unitType, unitId) {
+ return {
+ type: DELETE_UNIT,
+ machineId,
+ unitType,
+ unitId,
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/rack.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/rack.js
new file mode 100644
index 00000000..1f65952a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/rack.js
@@ -0,0 +1,36 @@
+import { v4 as uuid } from 'uuid'
+
+export const EDIT_RACK_NAME = 'EDIT_RACK_NAME'
+export const DELETE_RACK = 'DELETE_RACK'
+export const ADD_MACHINE = 'ADD_MACHINE'
+
+export function editRackName(rackId, name) {
+ return {
+ type: EDIT_RACK_NAME,
+ name,
+ rackId,
+ }
+}
+
+export function deleteRack(tileId, rackId) {
+ return {
+ type: DELETE_RACK,
+ rackId,
+ tileId,
+ }
+}
+
+export function addMachine(rackId, position) {
+ return {
+ type: ADD_MACHINE,
+ machine: {
+ id: uuid(),
+ rackId,
+ position,
+ cpus: [],
+ gpus: [],
+ memories: [],
+ storages: [],
+ },
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js
new file mode 100644
index 00000000..14cc126c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js
@@ -0,0 +1,74 @@
+import { v4 as uuid } from 'uuid'
+import {
+ DEFAULT_RACK_SLOT_CAPACITY,
+ DEFAULT_RACK_POWER_CAPACITY,
+} from '../../../components/topologies/map/MapConstants'
+import { findTileWithPosition } from '../../../util/tile-calculations'
+
+export const ADD_ROOM = 'ADD_ROOM'
+export const EDIT_ROOM_NAME = 'EDIT_ROOM_NAME'
+export const DELETE_ROOM = 'DELETE_ROOM'
+export const START_RACK_CONSTRUCTION = 'START_RACK_CONSTRUCTION'
+export const STOP_RACK_CONSTRUCTION = 'STOP_RACK_CONSTRUCTION'
+export const ADD_RACK_TO_TILE = 'ADD_RACK_TO_TILE'
+
+export function addRoom(topologyId, room) {
+ return {
+ type: ADD_ROOM,
+ room: {
+ id: uuid(),
+ topologyId,
+ ...room,
+ },
+ }
+}
+
+export function editRoomName(roomId, name) {
+ return {
+ type: EDIT_ROOM_NAME,
+ name,
+ roomId,
+ }
+}
+
+export function startRackConstruction() {
+ return {
+ type: START_RACK_CONSTRUCTION,
+ }
+}
+
+export function stopRackConstruction() {
+ return {
+ type: STOP_RACK_CONSTRUCTION,
+ }
+}
+
+export function addRackToTile(positionX, positionY) {
+ return (dispatch, getState) => {
+ const { topology, interactionLevel } = getState()
+ const currentRoom = topology.rooms[interactionLevel.roomId]
+ const tiles = currentRoom.tiles.map((tileId) => topology.tiles[tileId])
+ const tile = findTileWithPosition(tiles, positionX, positionY)
+
+ if (tile !== null) {
+ dispatch({
+ type: ADD_RACK_TO_TILE,
+ tileId: tile.id,
+ rack: {
+ id: uuid(),
+ name: 'Rack',
+ capacity: DEFAULT_RACK_SLOT_CAPACITY,
+ powerCapacityW: DEFAULT_RACK_POWER_CAPACITY,
+ machines: [],
+ },
+ })
+ }
+ }
+}
+
+export function deleteRoom(roomId) {
+ return {
+ type: DELETE_ROOM,
+ roomId,
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/index.js
new file mode 100644
index 00000000..53cd9144
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/index.js
@@ -0,0 +1,59 @@
+import { useMemo } from 'react'
+import { applyMiddleware, compose, createStore } from 'redux'
+import { createLogger } from 'redux-logger'
+import createSagaMiddleware from 'redux-saga'
+import thunk from 'redux-thunk'
+import rootReducer from './reducers'
+import rootSaga from './sagas'
+import { createReduxEnhancer } from '@sentry/react'
+import { sentryDsn } from '../config'
+
+let store
+
+function initStore(initialState, ctx) {
+ const sagaMiddleware = createSagaMiddleware({ context: ctx })
+
+ const middlewares = [thunk, sagaMiddleware]
+
+ if (process.env.NODE_ENV !== 'production') {
+ middlewares.push(createLogger())
+ }
+
+ let middleware = applyMiddleware(...middlewares)
+
+ if (sentryDsn) {
+ middleware = compose(middleware, createReduxEnhancer())
+ }
+
+ const configuredStore = createStore(rootReducer, initialState, middleware)
+ sagaMiddleware.run(rootSaga)
+ store = configuredStore
+
+ return configuredStore
+}
+
+export const initializeStore = (preloadedState, ctx) => {
+ let _store = store ?? initStore(preloadedState, ctx)
+
+ // After navigating to a page with an initial Redux state, merge that state
+ // with the current state in the store, and create a new store
+ if (preloadedState && store) {
+ _store = initStore({
+ ...store.getState(),
+ ...preloadedState,
+ })
+ // Reset the current store
+ store = undefined
+ }
+
+ // For SSG and SSR always create a new store
+ if (typeof window === 'undefined') return _store
+ // Create the store once in the client
+ if (!store) store = _store
+
+ return _store
+}
+
+export function useStore(initialState, ctx) {
+ return useMemo(() => initializeStore(initialState, ctx), [initialState, ctx])
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js
new file mode 100644
index 00000000..d0aac5ae
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js
@@ -0,0 +1,43 @@
+import { combineReducers } from 'redux'
+import { GO_DOWN_ONE_INTERACTION_LEVEL } from '../actions/interaction-level'
+import {
+ CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED,
+ FINISH_NEW_ROOM_CONSTRUCTION,
+ FINISH_ROOM_EDIT,
+ START_NEW_ROOM_CONSTRUCTION_SUCCEEDED,
+ START_ROOM_EDIT,
+} from '../actions/topology/building'
+import { DELETE_ROOM, START_RACK_CONSTRUCTION, STOP_RACK_CONSTRUCTION } from '../actions/topology/room'
+
+export function currentRoomInConstruction(state = '-1', action) {
+ switch (action.type) {
+ case START_NEW_ROOM_CONSTRUCTION_SUCCEEDED:
+ return action.roomId
+ case START_ROOM_EDIT:
+ return action.roomId
+ case CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED:
+ case FINISH_NEW_ROOM_CONSTRUCTION:
+ case FINISH_ROOM_EDIT:
+ case DELETE_ROOM:
+ return '-1'
+ default:
+ return state
+ }
+}
+
+export function inRackConstructionMode(state = false, action) {
+ switch (action.type) {
+ case START_RACK_CONSTRUCTION:
+ return true
+ case STOP_RACK_CONSTRUCTION:
+ case GO_DOWN_ONE_INTERACTION_LEVEL:
+ return false
+ default:
+ return state
+ }
+}
+
+export const construction = combineReducers({
+ currentRoomInConstruction,
+ inRackConstructionMode,
+})
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/index.js
new file mode 100644
index 00000000..7ffb1211
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/index.js
@@ -0,0 +1,12 @@
+import { combineReducers } from 'redux'
+import { construction } from './construction-mode'
+import { interactionLevel } from './interaction-level'
+import topology from './topology'
+
+const rootReducer = combineReducers({
+ topology,
+ construction,
+ interactionLevel,
+})
+
+export default rootReducer
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/interaction-level.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/interaction-level.js
new file mode 100644
index 00000000..b30c68b9
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/interaction-level.js
@@ -0,0 +1,68 @@
+import {
+ GO_DOWN_ONE_INTERACTION_LEVEL,
+ GO_FROM_BUILDING_TO_ROOM,
+ GO_FROM_RACK_TO_MACHINE,
+ GO_FROM_ROOM_TO_RACK,
+} from '../actions/interaction-level'
+import { DELETE_MACHINE } from '../actions/topology/machine'
+import { DELETE_RACK } from '../actions/topology/rack'
+import { DELETE_ROOM } from '../actions/topology/room'
+
+export function interactionLevel(state = { mode: 'BUILDING' }, action) {
+ switch (action.type) {
+ case GO_FROM_BUILDING_TO_ROOM:
+ return {
+ mode: 'ROOM',
+ roomId: action.roomId,
+ }
+ case GO_FROM_ROOM_TO_RACK:
+ return {
+ mode: 'RACK',
+ roomId: state.roomId,
+ tileId: action.tileId,
+ }
+ case GO_FROM_RACK_TO_MACHINE:
+ return {
+ mode: 'MACHINE',
+ roomId: state.roomId,
+ tileId: state.tileId,
+ position: action.position,
+ }
+ case GO_DOWN_ONE_INTERACTION_LEVEL:
+ if (state.mode === 'ROOM') {
+ return {
+ mode: 'BUILDING',
+ }
+ } else if (state.mode === 'RACK') {
+ return {
+ mode: 'ROOM',
+ roomId: state.roomId,
+ }
+ } else if (state.mode === 'MACHINE') {
+ return {
+ mode: 'RACK',
+ roomId: state.roomId,
+ tileId: state.tileId,
+ }
+ } else {
+ return state
+ }
+ case DELETE_MACHINE:
+ return {
+ mode: 'RACK',
+ roomId: state.roomId,
+ tileId: state.tileId,
+ }
+ case DELETE_RACK:
+ return {
+ mode: 'ROOM',
+ roomId: state.roomId,
+ }
+ case DELETE_ROOM:
+ return {
+ mode: 'BUILDING',
+ }
+ default:
+ return state
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/index.js
new file mode 100644
index 00000000..2c849387
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/index.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { CPU_UNITS, GPU_UNITS, MEMORY_UNITS, STORAGE_UNITS } from '../../../util/unit-specifications'
+import machine from './machine'
+import rack from './rack'
+import room from './room'
+import tile from './tile'
+import topology from './topology'
+
+function objects(state = {}, action) {
+ return {
+ cpus: CPU_UNITS,
+ gpus: GPU_UNITS,
+ memories: MEMORY_UNITS,
+ storages: STORAGE_UNITS,
+ machines: machine(state.machines, action, state),
+ racks: rack(state.racks, action, state),
+ tiles: tile(state.tiles, action),
+ rooms: room(state.rooms, action, state),
+ root: topology(state.root, action, state),
+ }
+}
+
+export default objects
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js
new file mode 100644
index 00000000..1789257b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js
@@ -0,0 +1,47 @@
+import produce from 'immer'
+import { STORE_TOPOLOGY } from '../../actions/topology'
+import { DELETE_MACHINE, ADD_UNIT, DELETE_UNIT } from '../../actions/topology/machine'
+import { ADD_MACHINE, DELETE_RACK } from '../../actions/topology/rack'
+
+function machine(state = {}, action, { racks }) {
+ switch (action.type) {
+ case STORE_TOPOLOGY:
+ return action.entities.machines || {}
+ case ADD_MACHINE:
+ return produce(state, (draft) => {
+ const { machine } = action
+ draft[machine.id] = machine
+ })
+ case DELETE_MACHINE:
+ return produce(state, (draft) => {
+ const { machineId } = action
+ delete draft[machineId]
+ })
+ case ADD_UNIT:
+ return produce(state, (draft) => {
+ const { machineId, unitType, unitId } = action
+ draft[machineId][unitType].push(unitId)
+ })
+ case DELETE_UNIT:
+ return produce(state, (draft) => {
+ const { machineId, unitType, unitId } = action
+ const units = draft[machineId][unitType]
+ const index = units.indexOf(unitId)
+ units.splice(index, 1)
+ })
+ case DELETE_RACK:
+ return produce(state, (draft) => {
+ const { rackId } = action
+ const rack = racks[rackId]
+
+ for (const id of rack.machines) {
+ const machine = draft[id]
+ machine.rackId = undefined
+ }
+ })
+ default:
+ return state
+ }
+}
+
+export default machine
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/rack.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/rack.js
new file mode 100644
index 00000000..ca79348a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/rack.js
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import produce from 'immer'
+import { STORE_TOPOLOGY } from '../../actions/topology'
+import { DELETE_MACHINE } from '../../actions/topology/machine'
+import { DELETE_RACK, EDIT_RACK_NAME, ADD_MACHINE } from '../../actions/topology/rack'
+import { ADD_RACK_TO_TILE } from '../../actions/topology/room'
+
+function rack(state = {}, action, { machines }) {
+ switch (action.type) {
+ case STORE_TOPOLOGY:
+ return action.entities.racks || {}
+ case ADD_RACK_TO_TILE:
+ return produce(state, (draft) => {
+ const { rack } = action
+ draft[rack.id] = rack
+ })
+ case EDIT_RACK_NAME:
+ return produce(state, (draft) => {
+ const { rackId, name } = action
+ draft[rackId].name = name
+ })
+ case DELETE_RACK:
+ return produce(state, (draft) => {
+ const { rackId } = action
+ delete draft[rackId]
+ })
+ case ADD_MACHINE:
+ return produce(state, (draft) => {
+ const { machine } = action
+ draft[machine.rackId].machines.push(machine.id)
+ })
+ case DELETE_MACHINE:
+ return produce(state, (draft) => {
+ const { machineId } = action
+ const machine = machines[machineId]
+ const rack = draft[machine.rackId]
+ const index = rack.machines.indexOf(machineId)
+ rack.machines.splice(index, 1)
+ })
+ default:
+ return state
+ }
+}
+
+export default rack
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/room.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/room.js
new file mode 100644
index 00000000..c05c8bfa
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/room.js
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import produce from 'immer'
+import { STORE_TOPOLOGY } from '../../actions/topology'
+import { ADD_TILE, DELETE_TILE } from '../../actions/topology/building'
+import { DELETE_ROOM, EDIT_ROOM_NAME, ADD_ROOM } from '../../actions/topology/room'
+
+function room(state = {}, action, { tiles }) {
+ switch (action.type) {
+ case STORE_TOPOLOGY:
+ return action.entities.rooms || {}
+ case ADD_ROOM:
+ return produce(state, (draft) => {
+ const { room } = action
+ draft[room.id] = room
+ })
+ case DELETE_ROOM:
+ return produce(state, (draft) => {
+ const { roomId } = action
+ delete draft[roomId]
+ })
+ case EDIT_ROOM_NAME:
+ return produce(state, (draft) => {
+ const { roomId, name } = action
+ draft[roomId].name = name
+ })
+ case ADD_TILE:
+ return produce(state, (draft) => {
+ const { tile } = action
+ draft[tile.roomId].tiles.push(tile.id)
+ })
+ case DELETE_TILE:
+ return produce(state, (draft) => {
+ const { tileId } = action
+ const tile = tiles[tileId]
+ const room = draft[tile.roomId]
+ const index = room.tiles.indexOf(tileId)
+ room.tiles.splice(index, 1)
+ })
+ default:
+ return state
+ }
+}
+
+export default room
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/tile.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/tile.js
new file mode 100644
index 00000000..24c0e20c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/tile.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import produce from 'immer'
+import { STORE_TOPOLOGY } from '../../actions/topology'
+import { ADD_TILE, DELETE_TILE } from '../../actions/topology/building'
+import { DELETE_RACK } from '../../actions/topology/rack'
+import { ADD_RACK_TO_TILE } from '../../actions/topology/room'
+
+function tile(state = {}, action) {
+ switch (action.type) {
+ case STORE_TOPOLOGY:
+ return action.entities.tiles || {}
+ case ADD_TILE:
+ return produce(state, (draft) => {
+ const { tile } = action
+ draft[tile.id] = tile
+ })
+ case DELETE_TILE:
+ return produce(state, (draft) => {
+ const { tileId } = action
+ delete draft[tileId]
+ })
+ case ADD_RACK_TO_TILE:
+ return produce(state, (draft) => {
+ const { rack, tileId } = action
+ draft[tileId].rack = rack.id
+ })
+ case DELETE_RACK:
+ return produce(state, (draft) => {
+ const { tileId } = action
+ draft[tileId].rack = undefined
+ })
+ default:
+ return state
+ }
+}
+
+export default tile
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/topology.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/topology.js
new file mode 100644
index 00000000..dff0a69e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/topology.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import produce from 'immer'
+import { STORE_TOPOLOGY } from '../../actions/topology'
+import { ADD_ROOM, DELETE_ROOM } from '../../actions/topology/room'
+
+function topology(state = undefined, action) {
+ switch (action.type) {
+ case STORE_TOPOLOGY:
+ return action.topology
+ case ADD_ROOM:
+ return produce(state, (draft) => {
+ const { room } = action
+ draft.rooms.push(room.id)
+ })
+ case DELETE_ROOM:
+ return produce(state, (draft) => {
+ const { roomId } = action
+ const index = draft.rooms.indexOf(roomId)
+ draft.rooms.splice(index, 1)
+ })
+ default:
+ return state
+ }
+}
+
+export default topology
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/sagas/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/sagas/index.js
new file mode 100644
index 00000000..0fabdb6d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/sagas/index.js
@@ -0,0 +1,7 @@
+import { fork } from 'redux-saga/effects'
+import { watchServer, updateServer } from './topology'
+
+export default function* rootSaga() {
+ yield fork(watchServer)
+ yield fork(updateServer)
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/sagas/topology.js b/opendc-web/opendc-web-server/src/main/webui/redux/sagas/topology.js
new file mode 100644
index 00000000..15147bcf
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/redux/sagas/topology.js
@@ -0,0 +1,76 @@
+import { QueryObserver, MutationObserver } from 'react-query'
+import { normalize, denormalize } from 'normalizr'
+import { select, put, take, race, getContext, call } from 'redux-saga/effects'
+import { eventChannel } from 'redux-saga'
+import { Topology } from '../../util/topology-schema'
+import { storeTopology, OPEN_TOPOLOGY } from '../actions/topology'
+
+/**
+ * Update the topology on the server.
+ */
+export function* updateServer() {
+ const queryClient = yield getContext('queryClient')
+ const mutationObserver = new MutationObserver(queryClient, { mutationKey: 'updateTopology' })
+
+ while (true) {
+ yield take(
+ (action) =>
+ action.type.startsWith('EDIT') || action.type.startsWith('ADD') || action.type.startsWith('DELETE')
+ )
+ const topology = yield select((state) => state.topology)
+
+ if (!topology.root) {
+ continue
+ }
+
+ const denormalizedTopology = denormalize(topology.root, Topology, topology)
+ yield call([mutationObserver, mutationObserver.mutate], denormalizedTopology)
+ }
+}
+
+/**
+ * Watch the topology on the server for changes.
+ */
+export function* watchServer() {
+ let { projectId, id } = yield take(OPEN_TOPOLOGY)
+ while (true) {
+ const channel = yield queryObserver(projectId, id)
+
+ while (true) {
+ const [action, response] = yield race([take(OPEN_TOPOLOGY), take(channel)])
+
+ if (action) {
+ projectId = action.projectId
+ id = action.id
+ break
+ }
+
+ const { isFetched, data } = response
+ // Only update the topology on the client-side when a new topology was fetched
+ if (isFetched) {
+ const { result: topologyId, entities } = normalize(data, Topology)
+ yield put(storeTopology(entities.topologies[topologyId], entities))
+ }
+ }
+ }
+}
+
+/**
+ * Observe changes for the topology with the specified identifier.
+ */
+function* queryObserver(projectId, id) {
+ const queryClient = yield getContext('queryClient')
+ const observer = new QueryObserver(queryClient, { queryKey: ['topologies', projectId, id] })
+
+ return eventChannel((emitter) => {
+ const unsubscribe = observer.subscribe((result) => {
+ emitter(result)
+ })
+
+ // Update result to make sure we did not miss any query updates
+ // between creating the observer and subscribing to it.
+ observer.updateResult()
+
+ return unsubscribe
+ })
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/shapes.js b/opendc-web/opendc-web-server/src/main/webui/shapes.js
new file mode 100644
index 00000000..50b82361
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/shapes.js
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+
+export const ProjectRole = PropTypes.oneOf(['VIEWER', 'EDITOR', 'OWNER'])
+
+export const Project = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ createdAt: PropTypes.string.isRequired,
+ updatedAt: PropTypes.string.isRequired,
+ role: ProjectRole,
+})
+
+export const ProcessingUnit = PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ clockRateMhz: PropTypes.number.isRequired,
+ numberOfCores: PropTypes.number.isRequired,
+ energyConsumptionW: PropTypes.number.isRequired,
+})
+
+export const StorageUnit = PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ speedMbPerS: PropTypes.number.isRequired,
+ sizeMb: PropTypes.number.isRequired,
+ energyConsumptionW: PropTypes.number.isRequired,
+})
+
+export const Machine = PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ position: PropTypes.number.isRequired,
+ cpus: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, PropTypes.string])),
+ gpus: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, PropTypes.string])),
+ memories: PropTypes.arrayOf(PropTypes.oneOfType([StorageUnit, PropTypes.string])),
+ storages: PropTypes.arrayOf(PropTypes.oneOfType([StorageUnit, PropTypes.string])),
+})
+
+export const Rack = PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ capacity: PropTypes.number.isRequired,
+ powerCapacityW: PropTypes.number.isRequired,
+ machines: PropTypes.arrayOf(PropTypes.oneOfType([Machine, PropTypes.string])),
+})
+
+export const Tile = PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ positionX: PropTypes.number.isRequired,
+ positionY: PropTypes.number.isRequired,
+ rack: PropTypes.oneOfType([Rack, PropTypes.string]),
+})
+
+export const Room = PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ tiles: PropTypes.arrayOf(PropTypes.oneOfType([Tile, PropTypes.string])),
+})
+
+export const Topology = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ project: Project.isRequired,
+ name: PropTypes.string.isRequired,
+ rooms: PropTypes.arrayOf(PropTypes.oneOfType([Room, PropTypes.string])),
+})
+
+export const Phenomena = PropTypes.shape({
+ failures: PropTypes.bool.isRequired,
+ interference: PropTypes.bool.isRequired,
+})
+
+export const Scheduler = PropTypes.shape({
+ name: PropTypes.string.isRequired,
+})
+
+export const Trace = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ type: PropTypes.string.isRequired,
+})
+
+export const Workload = PropTypes.shape({
+ trace: Trace.isRequired,
+ samplingFraction: PropTypes.number.isRequired,
+})
+
+export const Targets = PropTypes.shape({
+ repeats: PropTypes.number.isRequired,
+ metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
+})
+
+export const TopologySummary = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ project: Project.isRequired,
+ name: PropTypes.string.isRequired,
+})
+
+export const PortfolioSummary = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ project: Project.isRequired,
+ name: PropTypes.string.isRequired,
+ targets: PropTypes.shape({
+ repeats: PropTypes.number.isRequired,
+ metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
+ }).isRequired,
+})
+
+export const ScenarioSummary = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ workload: Workload.isRequired,
+ topology: TopologySummary.isRequired,
+ phenomena: Phenomena.isRequired,
+ schedulerName: PropTypes.string.isRequired,
+ results: PropTypes.object,
+})
+
+export const JobState = PropTypes.oneOf(['PENDING', 'CLAIMED', 'RUNNING', 'FAILED', 'FINISHED'])
+
+export const Job = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ state: JobState.isRequired,
+ createdAt: PropTypes.string.isRequired,
+ updatedAt: PropTypes.string.isRequired,
+ results: PropTypes.object,
+})
+
+export const Scenario = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ project: Project.isRequired,
+ portfolio: PortfolioSummary.isRequired,
+ name: PropTypes.string.isRequired,
+ workload: Workload.isRequired,
+ topology: TopologySummary.isRequired,
+ phenomena: Phenomena.isRequired,
+ schedulerName: PropTypes.string.isRequired,
+ jobs: PropTypes.arrayOf(Job).isRequired,
+})
+
+export const Portfolio = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ project: Project.isRequired,
+ targets: Targets.isRequired,
+ scenarios: PropTypes.arrayOf(ScenarioSummary).isRequired,
+})
+
+export const WallSegment = PropTypes.shape({
+ startPosX: PropTypes.number.isRequired,
+ startPosY: PropTypes.number.isRequired,
+ isHorizontal: PropTypes.bool.isRequired,
+ length: PropTypes.number.isRequired,
+})
+
+export const InteractionLevel = PropTypes.shape({
+ mode: PropTypes.string.isRequired,
+ roomId: PropTypes.string,
+ rackId: PropTypes.string,
+})
+
+export const Status = PropTypes.oneOf(['idle', 'loading', 'error', 'success'])
diff --git a/opendc-web/opendc-web-server/src/main/webui/style/index.css b/opendc-web/opendc-web-server/src/main/webui/style/index.css
new file mode 100644
index 00000000..7b7103a4
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/style/index.css
@@ -0,0 +1,28 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+body,
+#__next {
+ height: 100%;
+
+ background: #eee;
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/authorizations.js b/opendc-web/opendc-web-server/src/main/webui/util/authorizations.js
new file mode 100644
index 00000000..6cb08ba8
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/authorizations.js
@@ -0,0 +1,21 @@
+import HomeIcon from '@patternfly/react-icons/dist/js/icons/home-icon'
+import EditIcon from '@patternfly/react-icons/dist/js/icons/edit-icon'
+import EyeIcon from '@patternfly/react-icons/dist/js/icons/eye-icon'
+
+export const AUTH_ICON_MAP = {
+ OWNER: HomeIcon,
+ EDITOR: EditIcon,
+ VIEWER: EyeIcon,
+}
+
+export const AUTH_NAME_MAP = {
+ OWNER: 'Owner',
+ EDITOR: 'Editor',
+ VIEWER: 'Viewer',
+}
+
+export const AUTH_DESCRIPTION_MAP = {
+ OWNER: 'You own this project',
+ EDITOR: 'You can edit this project',
+ VIEWER: 'You can view this project',
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/available-metrics.js b/opendc-web/opendc-web-server/src/main/webui/util/available-metrics.js
new file mode 100644
index 00000000..fda6cd4d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/available-metrics.js
@@ -0,0 +1,101 @@
+export const METRIC_GROUPS = {
+ 'Host Metrics': [
+ 'total_overcommitted_burst',
+ 'total_power_draw',
+ 'total_failure_vm_slices',
+ 'total_granted_burst',
+ 'total_interfered_burst',
+ 'total_requested_burst',
+ 'mean_cpu_usage',
+ 'mean_cpu_demand',
+ 'mean_num_deployed_images',
+ 'max_num_deployed_images',
+ ],
+ 'Compute Service Metrics': ['total_vms_submitted', 'total_vms_queued', 'total_vms_finished', 'total_vms_failed'],
+}
+
+export const AVAILABLE_METRICS = [
+ 'mean_cpu_usage',
+ 'mean_cpu_demand',
+ 'total_requested_burst',
+ 'total_granted_burst',
+ 'total_overcommitted_burst',
+ 'total_interfered_burst',
+ 'total_power_draw',
+ 'total_failure_vm_slices',
+ 'mean_num_deployed_images',
+ 'max_num_deployed_images',
+ 'total_vms_submitted',
+ 'total_vms_queued',
+ 'total_vms_finished',
+ 'total_vms_failed',
+]
+
+export const METRIC_NAMES_SHORT = {
+ total_overcommitted_burst: 'Overcomm. CPU Cycles',
+ total_granted_burst: 'Granted CPU Cycles',
+ total_requested_burst: 'Requested CPU Cycles',
+ total_interfered_burst: 'Interfered CPU Cycles',
+ total_power_draw: 'Total Power Consumption',
+ mean_cpu_usage: 'Mean Host CPU Usage',
+ mean_cpu_demand: 'Mean Host CPU Demand',
+ mean_num_deployed_images: 'Mean Num. Deployed Images Per Host',
+ max_num_deployed_images: 'Max. Num. Deployed Images Per Host',
+ total_failure_vm_slices: 'Total Num. Failed VM Slices',
+ total_vms_submitted: 'VMs Submitted',
+ total_vms_queued: 'VMs Queued',
+ total_vms_finished: 'VMs Finished',
+ total_vms_failed: 'VMs Failed',
+}
+
+export const METRIC_NAMES = {
+ total_overcommitted_burst: 'Overcommitted CPU Cycles',
+ total_granted_burst: 'Granted CPU Cycles',
+ total_requested_burst: 'Requested CPU Cycles',
+ total_interfered_burst: 'Interfered CPU Cycles',
+ total_power_draw: 'Total Power Consumption',
+ mean_cpu_usage: 'Mean Host CPU Usage',
+ mean_cpu_demand: 'Mean Host CPU Demand',
+ mean_num_deployed_images: 'Mean Number of Deployed Images Per Host',
+ max_num_deployed_images: 'Maximum Number Deployed Images Per Host',
+ total_failure_vm_slices: 'Failed VM Slices',
+ total_vms_submitted: 'VMs Submitted',
+ total_vms_queued: 'VMs Queued',
+ total_vms_finished: 'VMs Finished',
+ total_vms_failed: 'VMs Failed',
+}
+
+export const METRIC_UNITS = {
+ total_overcommitted_burst: 'MFLOP',
+ total_granted_burst: 'MFLOP',
+ total_requested_burst: 'MFLOP',
+ total_interfered_burst: 'MFLOP',
+ total_power_draw: 'Wh',
+ mean_cpu_usage: 'MHz',
+ mean_cpu_demand: 'MHz',
+ mean_num_deployed_images: 'VMs',
+ max_num_deployed_images: 'VMs',
+ total_failure_vm_slices: 'VM Slices',
+ total_vms_submitted: 'VMs',
+ total_vms_queued: 'VMs',
+ total_vms_finished: 'VMs',
+ total_vms_failed: 'VMs',
+}
+
+export const METRIC_DESCRIPTIONS = {
+ total_overcommitted_burst:
+ 'The total CPU clock cycles lost due to overcommitting of resources. This metric is an indicator for resource overload.',
+ total_requested_burst: 'The total CPU clock cycles that were requested by all virtual machines.',
+ total_granted_burst: 'The total CPU clock cycles executed by the hosts.',
+ total_interfered_burst: 'The total CPU clock cycles lost due to resource interference between virtual machines.',
+ total_power_draw: 'The average power usage in watts.',
+ mean_cpu_usage: 'The average amount of CPU clock cycles consumed by all virtual machines on a host.',
+ mean_cpu_demand: 'The average amount of CPU clock cycles requested by all powered on virtual machines on a host.',
+ mean_num_deployed_images: 'The average number of virtual machines deployed on a host.',
+ max_num_deployed_images: 'The maximum number of virtual machines deployed at any time.',
+ total_failure_vm_slices: 'The total amount of CPU clock cycles lost due to failure.',
+ total_vms_submitted: 'The number of virtual machines scheduled by the compute service.',
+ total_vms_queued: 'The number of virtual machines still waiting to be scheduled by the compute service.',
+ total_vms_finished: 'The number of virtual machines that completed.',
+ total_vms_failed: 'The number of virtual machines that could not be scheduled.',
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/colors.js b/opendc-web/opendc-web-server/src/main/webui/util/colors.js
new file mode 100644
index 00000000..34468503
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/colors.js
@@ -0,0 +1,29 @@
+export const GRID_COLOR = 'rgba(0, 0, 0, 0.5)'
+export const BACKDROP_COLOR = 'rgba(255, 255, 255, 1)'
+export const WALL_COLOR = 'rgba(0, 0, 0, 1)'
+
+export const ROOM_DEFAULT_COLOR = 'rgba(150, 150, 150, 1)'
+export const ROOM_IN_CONSTRUCTION_COLOR = 'rgba(51, 153, 255, 1)'
+export const ROOM_HOVER_VALID_COLOR = 'rgba(51, 153, 255, 1)'
+export const ROOM_HOVER_INVALID_COLOR = 'rgba(255, 102, 0, 1)'
+export const ROOM_NAME_COLOR = 'rgba(245, 245, 245, 1)'
+export const ROOM_TYPE_COLOR = 'rgba(245, 245, 245, 1)'
+
+export const TILE_PLUS_COLOR = 'rgba(0, 0, 0, 1)'
+
+export const OBJECT_BORDER_COLOR = 'rgba(0, 0, 0, 1)'
+
+export const RACK_BACKGROUND_COLOR = 'rgba(170, 170, 170, 1)'
+export const RACK_SPACE_BAR_BACKGROUND_COLOR = 'rgba(222, 235, 247, 0.6)'
+export const RACK_SPACE_BAR_FILL_COLOR = 'rgba(91, 155, 213, 0.7)'
+export const RACK_ENERGY_BAR_BACKGROUND_COLOR = 'rgba(255, 242, 204, 0.6)'
+export const RACK_ENERGY_BAR_FILL_COLOR = 'rgba(244, 215, 0, 0.7)'
+export const COOLING_ITEM_BACKGROUND_COLOR = 'rgba(40, 50, 230, 1)'
+export const PSU_BACKGROUND_COLOR = 'rgba(230, 50, 60, 1)'
+
+export const GRAYED_OUT_AREA_COLOR = 'rgba(0, 0, 0, 0.6)'
+
+export const SIM_LOW_COLOR = 'rgba(197, 224, 180, 1)'
+export const SIM_MID_LOW_COLOR = 'rgba(255, 230, 153, 1)'
+export const SIM_MID_HIGH_COLOR = 'rgba(248, 203, 173, 1)'
+export const SIM_HIGH_COLOR = 'rgba(249, 165, 165, 1)'
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/date-time.js b/opendc-web/opendc-web-server/src/main/webui/util/date-time.js
new file mode 100644
index 00000000..7e2f6623
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/date-time.js
@@ -0,0 +1,81 @@
+/**
+ * Parses and formats the given date-time string representation.
+ *
+ * The format assumed is "YYYY-MM-DDTHH:MM:SS".
+ *
+ * @param dateTimeString A string expressing a date and a time, in the above mentioned format.
+ * @returns {string} A human-friendly string version of that date and time.
+ */
+export function parseAndFormatDateTime(dateTimeString) {
+ return formatDateTime(new Date(dateTimeString))
+}
+
+/**
+ * Serializes the given date and time value to a human-friendly string.
+ *
+ * @param dateTime An object representation of a date and time.
+ * @returns {string} A human-friendly string version of that date and time.
+ */
+export function formatDateTime(dateTime) {
+ let date
+ const currentDate = new Date()
+
+ date =
+ addPaddingToTwo(dateTime.getDay()) +
+ '/' +
+ addPaddingToTwo(dateTime.getMonth()) +
+ '/' +
+ addPaddingToTwo(dateTime.getFullYear())
+
+ if (dateTime.getFullYear() === currentDate.getFullYear() && dateTime.getMonth() === currentDate.getMonth()) {
+ if (dateTime.getDate() === currentDate.getDate()) {
+ date = 'Today'
+ } else if (dateTime.getDate() === currentDate.getDate() - 1) {
+ date = 'Yesterday'
+ }
+ }
+
+ return date + ', ' + addPaddingToTwo(dateTime.getHours()) + ':' + addPaddingToTwo(dateTime.getMinutes())
+}
+
+/**
+ * Formats the given number of seconds/ticks to a formatted time representation.
+ *
+ * @param seconds The number of seconds.
+ * @returns {string} A string representation of that amount of second, in the from of HH:MM:SS.
+ */
+export function convertSecondsToFormattedTime(seconds) {
+ if (seconds <= 0) {
+ return '0s'
+ }
+
+ let hour = Math.floor(seconds / 3600)
+ let minute = Math.floor(seconds / 60) % 60
+ let second = seconds % 60
+
+ hour = isNaN(hour) ? 0 : hour
+ minute = isNaN(minute) ? 0 : minute
+ second = isNaN(second) ? 0 : second
+
+ if (hour === 0 && minute === 0) {
+ return second + 's'
+ } else if (hour === 0) {
+ return minute + 'm' + addPaddingToTwo(second) + 's'
+ } else {
+ return hour + 'h' + addPaddingToTwo(minute) + 'm' + addPaddingToTwo(second) + 's'
+ }
+}
+
+/**
+ * Pads the given integer to have at least two digits.
+ *
+ * @param integer An integer to be padded.
+ * @returns {string} A string containing the padded integer.
+ */
+function addPaddingToTwo(integer) {
+ if (integer < 10) {
+ return '0' + integer.toString()
+ } else {
+ return integer.toString()
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/date-time.test.js b/opendc-web/opendc-web-server/src/main/webui/util/date-time.test.js
new file mode 100644
index 00000000..431e39f7
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/date-time.test.js
@@ -0,0 +1,21 @@
+import { convertSecondsToFormattedTime } from './date-time'
+
+describe('tick formatting', () => {
+ it("returns '0s' for numbers <= 0", () => {
+ expect(convertSecondsToFormattedTime(-1)).toEqual('0s')
+ expect(convertSecondsToFormattedTime(0)).toEqual('0s')
+ })
+ it('returns only seconds for values under a minute', () => {
+ expect(convertSecondsToFormattedTime(1)).toEqual('1s')
+ expect(convertSecondsToFormattedTime(59)).toEqual('59s')
+ })
+ it('returns seconds and minutes for values under an hour', () => {
+ expect(convertSecondsToFormattedTime(60)).toEqual('1m00s')
+ expect(convertSecondsToFormattedTime(61)).toEqual('1m01s')
+ expect(convertSecondsToFormattedTime(3599)).toEqual('59m59s')
+ })
+ it('returns full time for values over an hour', () => {
+ expect(convertSecondsToFormattedTime(3600)).toEqual('1h00m00s')
+ expect(convertSecondsToFormattedTime(3601)).toEqual('1h00m01s')
+ })
+})
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/effect-ref.js b/opendc-web/opendc-web-server/src/main/webui/util/effect-ref.js
new file mode 100644
index 00000000..78528585
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/effect-ref.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useCallback, useRef } from 'react'
+
+const noop = () => {}
+
+/**
+ * A hook that will invoke the specified callback when the reference returned by this function is initialized.
+ * The callback can return an optional clean-up function.
+ */
+export function useEffectRef(callback, deps = []) {
+ const disposeRef = useRef(noop)
+ return useCallback((element) => {
+ disposeRef.current()
+ disposeRef.current = noop
+
+ if (element) {
+ disposeRef.current = callback(element) || noop
+ }
+ }, deps) // eslint-disable-line react-hooks/exhaustive-deps
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/tile-calculations.js b/opendc-web/opendc-web-server/src/main/webui/util/tile-calculations.js
new file mode 100644
index 00000000..374ca48c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/tile-calculations.js
@@ -0,0 +1,255 @@
+export function deriveWallLocations(tiles) {
+ const { verticalWalls, horizontalWalls } = getWallSegments(tiles)
+ return mergeWallSegments(verticalWalls, horizontalWalls)
+}
+
+function getWallSegments(tiles) {
+ const verticalWalls = {}
+ const horizontalWalls = {}
+
+ tiles.forEach((tile) => {
+ const x = tile.positionX,
+ y = tile.positionY
+
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue
+ }
+
+ let doInsert = true
+ for (const tile of tiles) {
+ if (tile.positionX === x + dX && tile.positionY === y + dY) {
+ doInsert = false
+ break
+ }
+ }
+ if (!doInsert) {
+ continue
+ }
+
+ if (dX === -1) {
+ if (verticalWalls[x] === undefined) {
+ verticalWalls[x] = []
+ }
+ if (verticalWalls[x].indexOf(y) === -1) {
+ verticalWalls[x].push(y)
+ }
+ } else if (dX === 1) {
+ if (verticalWalls[x + 1] === undefined) {
+ verticalWalls[x + 1] = []
+ }
+ if (verticalWalls[x + 1].indexOf(y) === -1) {
+ verticalWalls[x + 1].push(y)
+ }
+ } else if (dY === -1) {
+ if (horizontalWalls[y] === undefined) {
+ horizontalWalls[y] = []
+ }
+ if (horizontalWalls[y].indexOf(x) === -1) {
+ horizontalWalls[y].push(x)
+ }
+ } else if (dY === 1) {
+ if (horizontalWalls[y + 1] === undefined) {
+ horizontalWalls[y + 1] = []
+ }
+ if (horizontalWalls[y + 1].indexOf(x) === -1) {
+ horizontalWalls[y + 1].push(x)
+ }
+ }
+ }
+ }
+ })
+
+ return { verticalWalls, horizontalWalls }
+}
+
+function mergeWallSegments(vertical, horizontal) {
+ const result = []
+ const walls = [vertical, horizontal]
+
+ for (let i = 0; i < 2; i++) {
+ const wallList = walls[i]
+ for (let a in wallList) {
+ a = parseInt(a, 10)
+
+ wallList[a].sort((a, b) => {
+ return a - b
+ })
+
+ let startPos = wallList[a][0]
+ const isHorizontal = i === 1
+
+ if (wallList[a].length === 1) {
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: 1,
+ })
+ } else {
+ let consecutiveCount = 1
+ for (let b = 0; b < wallList[a].length - 1; b++) {
+ if (b + 1 === wallList[a].length - 1) {
+ if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: consecutiveCount,
+ })
+ consecutiveCount = 0
+ startPos = wallList[a][b + 1]
+ }
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: consecutiveCount + 1,
+ })
+ break
+ } else if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: consecutiveCount,
+ })
+ startPos = wallList[a][b + 1]
+ consecutiveCount = 0
+ }
+ consecutiveCount++
+ }
+ }
+ }
+ }
+
+ return result
+}
+
+export function deriveValidNextTilePositions(rooms, selectedTiles) {
+ const result = [],
+ newPosition = { x: 0, y: 0 }
+ let isSurroundingTile
+
+ selectedTiles.forEach((tile) => {
+ const x = tile.positionX
+ const y = tile.positionY
+ result.push({ x, y })
+
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue
+ }
+
+ newPosition.x = x + dX
+ newPosition.y = y + dY
+
+ isSurroundingTile = true
+ for (let index in selectedTiles) {
+ if (
+ selectedTiles[index].positionX === newPosition.x &&
+ selectedTiles[index].positionY === newPosition.y
+ ) {
+ isSurroundingTile = false
+ break
+ }
+ }
+
+ if (isSurroundingTile && findPositionInRooms(rooms, newPosition.x, newPosition.y) === -1) {
+ result.push({ x: newPosition.x, y: newPosition.y })
+ }
+ }
+ }
+ })
+
+ return result
+}
+
+export function findPositionInPositions(positions, positionX, positionY) {
+ for (let i = 0; i < positions.length; i++) {
+ const position = positions[i]
+ if (positionX === position.x && positionY === position.y) {
+ return i
+ }
+ }
+
+ return -1
+}
+
+export function findPositionInRooms(rooms, positionX, positionY) {
+ for (let i = 0; i < rooms.length; i++) {
+ const room = rooms[i]
+ if (findPositionInTiles(room.tiles, positionX, positionY) !== -1) {
+ return i
+ }
+ }
+
+ return -1
+}
+
+function findPositionInTiles(tiles, positionX, positionY) {
+ let index = -1
+
+ for (let i = 0; i < tiles.length; i++) {
+ const tile = tiles[i]
+ if (positionX === tile.positionX && positionY === tile.positionY) {
+ index = i
+ break
+ }
+ }
+
+ return index
+}
+
+export function findTileWithPosition(tiles, positionX, positionY) {
+ for (let i = 0; i < tiles.length; i++) {
+ if (tiles[i].positionX === positionX && tiles[i].positionY === positionY) {
+ return tiles[i]
+ }
+ }
+
+ return null
+}
+
+export function calculateRoomListBounds(rooms) {
+ const min = { x: Number.MAX_VALUE, y: Number.MAX_VALUE }
+ const max = { x: -1, y: -1 }
+
+ rooms.forEach((room) => {
+ room.tiles.forEach((tile) => {
+ if (tile.positionX < min.x) {
+ min.x = tile.positionX
+ }
+ if (tile.positionY < min.y) {
+ min.y = tile.positionY
+ }
+
+ if (tile.positionX > max.x) {
+ max.x = tile.positionX
+ }
+ if (tile.positionY > max.y) {
+ max.y = tile.positionY
+ }
+ })
+ })
+
+ max.x++
+ max.y++
+
+ const center = {
+ x: min.x + (max.x - min.x) / 2.0,
+ y: min.y + (max.y - min.y) / 2.0,
+ }
+
+ return { min, center, max }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/topology-schema.js b/opendc-web/opendc-web-server/src/main/webui/util/topology-schema.js
new file mode 100644
index 00000000..ff672dd6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/topology-schema.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { schema } from 'normalizr'
+
+const Cpu = new schema.Entity('cpus', {}, { idAttribute: 'id' })
+const Gpu = new schema.Entity('gpus', {}, { idAttribute: 'id' })
+const Memory = new schema.Entity('memories', {}, { idAttribute: 'id' })
+const Storage = new schema.Entity('storages', {}, { idAttribute: 'id' })
+
+export const Machine = new schema.Entity(
+ 'machines',
+ {
+ cpus: [Cpu],
+ gpus: [Gpu],
+ memories: [Memory],
+ storages: [Storage],
+ },
+ { idAttribute: 'id' }
+)
+
+export const Rack = new schema.Entity('racks', { machines: [Machine] }, { idAttribute: 'id' })
+
+export const Tile = new schema.Entity('tiles', { rack: Rack }, { idAttribute: 'id' })
+
+export const Room = new schema.Entity('rooms', { tiles: [Tile] }, { idAttribute: 'id' })
+
+export const Topology = new schema.Entity('topologies', { rooms: [Room] }, { idAttribute: 'id' })
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/unit-specifications.js b/opendc-web/opendc-web-server/src/main/webui/util/unit-specifications.js
new file mode 100644
index 00000000..3e3671cd
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/unit-specifications.js
@@ -0,0 +1,102 @@
+export const CPU_UNITS = {
+ 'cpu-1': {
+ id: 'cpu-1',
+ name: 'Intel i7 v6 6700k',
+ clockRateMhz: 4100,
+ numberOfCores: 4,
+ energyConsumptionW: 70,
+ },
+ 'cpu-2': {
+ id: 'cpu-2',
+ name: 'Intel i5 v6 6700k',
+ clockRateMhz: 3500,
+ numberOfCores: 2,
+ energyConsumptionW: 50,
+ },
+ 'cpu-3': {
+ id: 'cpu-3',
+ name: 'Intel® Xeon® E-2224G',
+ clockRateMhz: 3500,
+ numberOfCores: 4,
+ energyConsumptionW: 71,
+ },
+ 'cpu-4': {
+ id: 'cpu-4',
+ name: 'Intel® Xeon® E-2244G',
+ clockRateMhz: 3800,
+ numberOfCores: 8,
+ energyConsumptionW: 71,
+ },
+ 'cpu-5': {
+ id: 'cpu-5',
+ name: 'Intel® Xeon® E-2246G',
+ clockRateMhz: 3600,
+ numberOfCores: 12,
+ energyConsumptionW: 80,
+ },
+}
+
+export const GPU_UNITS = {
+ 'gpu-1': {
+ id: 'gpu-1',
+ name: 'NVIDIA GTX 4 1080',
+ clockRateMhz: 1200,
+ numberOfCores: 200,
+ energyConsumptionW: 250,
+ },
+ 'gpu-2': {
+ id: 'gpu-2',
+ name: 'NVIDIA Tesla V100',
+ clockRateMhz: 1200,
+ numberOfCores: 5120,
+ energyConsumptionW: 250,
+ },
+}
+
+export const MEMORY_UNITS = {
+ 'memory-1': {
+ id: 'memory-1',
+ name: 'Samsung PC DRAM K4A4G045WD',
+ speedMbPerS: 16000,
+ sizeMb: 4000,
+ energyConsumptionW: 10,
+ },
+ 'memory-2': {
+ id: 'memory-2',
+ name: 'Samsung PC DRAM M393A2K43BB1-CRC',
+ speedMbPerS: 2400,
+ sizeMb: 16000,
+ energyConsumptionW: 10,
+ },
+ 'memory-3': {
+ id: 'memory-3',
+ name: 'Crucial MTA18ASF4G72PDZ-3G2E1',
+ speedMbPerS: 3200,
+ sizeMb: 32000,
+ energyConsumptionW: 10,
+ },
+ 'memory-4': {
+ id: 'memory-4',
+ name: 'Crucial MTA9ASF2G72PZ-3G2E1',
+ speedMbPerS: 3200,
+ sizeMb: 16000,
+ energyConsumptionW: 10,
+ },
+}
+
+export const STORAGE_UNITS = {
+ 'storage-1': {
+ id: 'storage-1',
+ name: 'Samsung EVO 2016 SATA III',
+ speedMbPerS: 6000,
+ sizeMb: 250000,
+ energyConsumptionW: 10,
+ },
+ 'storage-2': {
+ id: 'storage-2',
+ name: 'Western Digital MTA9ASF2G72PZ-3G2E1',
+ speedMbPerS: 6000,
+ sizeMb: 4000000,
+ energyConsumptionW: 10,
+ },
+}
diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java
index 91e3eb66..f4a8c9d6 100644
--- a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java
+++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java
@@ -68,8 +68,8 @@ public class UserAccountingServiceTest {
var accounting = service.getAccounting(userId);
- assertTrue(accounting.getPeriodEnd().isAfter(LocalDate.now()));
- assertEquals(0, accounting.getSimulationTime());
+ assertTrue(accounting.periodEnd().isAfter(LocalDate.now()));
+ assertEquals(0, accounting.simulationTime());
}
@Test
@@ -85,9 +85,9 @@ public class UserAccountingServiceTest {
var accounting = service.getAccounting(userId);
assertAll(
- () -> assertEquals(periodEnd, accounting.getPeriodEnd()),
- () -> assertEquals(32, accounting.getSimulationTime()),
- () -> assertEquals(3600, accounting.getSimulationTimeBudget()));
+ () -> assertEquals(periodEnd, accounting.periodEnd()),
+ () -> assertEquals(32, accounting.simulationTime()),
+ () -> assertEquals(3600, accounting.simulationTimeBudget()));
}
@Test