diff options
Diffstat (limited to 'opendc-web/opendc-web-server/src')
213 files changed, 17711 insertions, 97 deletions
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 Binary files differnew file mode 100644 index 00000000..c2f40a0d --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/favicon.ico 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 Binary files differnew file mode 100644 index 00000000..ec2b7398 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/datacenter-drawing.png 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 Binary files differnew file mode 100644 index 00000000..d743038b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/logo.png 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 Binary files differnew file mode 100644 index 00000000..e0bf8e9b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/opendc-architecture.png 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 Binary files differnew file mode 100644 index 00000000..0b2821c4 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/opendc-timeline-v2.png 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 Binary files differnew file mode 100644 index 00000000..d2019b4d --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/aiosup.png 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 Binary files differnew file mode 100644 index 00000000..011c1627 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/evaneyk.png 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 Binary files differnew file mode 100644 index 00000000..218b1a6f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/fmastenbroek.png 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 Binary files differnew file mode 100644 index 00000000..96a3abda --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/gandreadis.png 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 Binary files differnew file mode 100644 index 00000000..4891c7f5 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/hhe.png 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 Binary files differnew file mode 100644 index 00000000..c76e1fab --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jbosch.png 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 Binary files differnew file mode 100644 index 00000000..d2691659 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/jburley.png 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 Binary files differnew file mode 100644 index 00000000..6fbc8472 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/lfdversluis.png 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 Binary files differnew file mode 100644 index 00000000..85865977 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/loverweel.png 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 Binary files differnew file mode 100644 index 00000000..41878161 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/sjounaid.png 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 Binary files differnew file mode 100644 index 00000000..4c8b3311 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/vvanbeek.png 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 Binary files differnew file mode 100644 index 00000000..c758846d --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/portraits/wlai.png 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 Binary files differnew file mode 100644 index 00000000..ea20a7c4 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-construction.png 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 Binary files differnew file mode 100644 index 00000000..1bd989c7 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/screenshot-simulation.png 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 Binary files differnew file mode 100644 index 00000000..d2638e6c --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Developer.png 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 Binary files differnew file mode 100644 index 00000000..92db7459 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Manager.png 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 Binary files differnew file mode 100644 index 00000000..d87edd39 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Researcher.png 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 Binary files differnew file mode 100644 index 00000000..5b7c3a72 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Sales.png 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 Binary files differnew file mode 100644 index 00000000..a4900303 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/stakeholders/Student.png 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 Binary files differnew file mode 100644 index 00000000..07cfbd31 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/cpu-icon.png 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 Binary files differnew file mode 100644 index 00000000..55d4fb05 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/gpu-icon.png 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 Binary files differnew file mode 100644 index 00000000..36e8a44e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/memory-icon.png 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 Binary files differnew file mode 100644 index 00000000..1088c61b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-energy-icon.png 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 Binary files differnew file mode 100644 index 00000000..387d7ea6 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/rack-space-icon.png 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 Binary files differnew file mode 100644 index 00000000..7a39cb6f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/topology/storage-icon.png 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 Binary files differnew file mode 100644 index 00000000..a7a2d56a --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/public/img/tudelft-icon.png 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 |
