summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-server
diff options
context:
space:
mode:
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