diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 50c8ed54..a6b4ec02 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,7 +11,7 @@ repos:
- id: debug-statements
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.4.8
+ rev: v0.5.6
hooks:
- id: ruff
args:
@@ -31,7 +31,7 @@ repos:
## ES
- repo: https://github.com/pre-commit/mirrors-eslint
- rev: v9.4.0
+ rev: v9.8.0
hooks:
- id: eslint
additional_dependencies:
diff --git a/backend/ibutsu_server/controllers/admin/portal_controller.py b/backend/ibutsu_server/controllers/admin/portal_controller.py
new file mode 100644
index 00000000..3dbe29b2
--- /dev/null
+++ b/backend/ibutsu_server/controllers/admin/portal_controller.py
@@ -0,0 +1,151 @@
+from http import HTTPStatus
+
+import connexion
+from flask import abort
+
+from ibutsu_server.constants import RESPONSE_JSON_REQ
+from ibutsu_server.db.base import session
+from ibutsu_server.db.models import Portal, User
+from ibutsu_server.filters import convert_filter
+from ibutsu_server.util.admin import check_user_is_admin
+from ibutsu_server.util.query import get_offset
+from ibutsu_server.util.uuid import convert_objectid_to_uuid, is_uuid, validate_uuid
+
+
+def admin_add_portal(portal=None, token_info=None, user=None) -> tuple[dict, int]:
+ """Create a portal
+
+ :param body: Portal
+ :type body: dict | bytes
+
+ :rtype: Portal
+ """
+ check_user_is_admin(user)
+ if not connexion.request.is_json:
+ return RESPONSE_JSON_REQ
+ portal = Portal.from_dict(**connexion.request.get_json())
+ # check if portal already exists
+ if portal.id and Portal.query.get(portal.id):
+ return f"Portal id {portal.id} already exist", HTTPStatus.BAD_REQUEST
+ if user := User.query.get(user):
+ portal.owner = user
+ session.add(portal)
+ session.commit()
+ return portal.to_dict(), HTTPStatus.CREATED
+
+
+@validate_uuid
+def admin_get_portal(id_, token_info=None, user=None) -> dict:
+ """Get a single portal by ID
+
+ :param id: ID of test portal
+ :type id: str
+
+ :rtype: Portal
+ """
+ check_user_is_admin(user)
+
+ # get by ID or check if the portal name matches the passed ID
+ if portal := Portal.query.get(id_) or Portal.query.filter(Portal.name == id_).first():
+ return portal.to_dict(with_owner=True)
+ else:
+ abort(HTTPStatus.NOT_FOUND)
+
+
+def admin_get_portal_list(
+ filter_=None,
+ owner_id=None,
+ group_id=None,
+ page=1,
+ page_size=25,
+ token_info=None,
+ user=None,
+) -> dict[list[dict], dict]:
+ """Get a list of portals
+
+ :param owner_id: Filter portals by owner ID
+ :type owner_id: str
+ :param group_id: Filter portals by group ID
+ :type group_id: str
+ :param limit: Limit the portals
+ :type limit: int
+ :param offset: Offset the portals
+ :type offset: int
+
+ :rtype: List[Portal]
+ """
+ check_user_is_admin(user)
+ query = Portal.query
+
+ if filter_:
+ for filter_string in filter_:
+ filter_clause = convert_filter(filter_string, Portal)
+ if filter_clause is not None:
+ query = query.filter(filter_clause)
+ if owner_id:
+ query = query.filter(Portal.owner_id == owner_id)
+ if group_id:
+ query = query.filter(Portal.group_id == group_id)
+
+ offset = get_offset(page, page_size)
+ total_items = query.count()
+ total_pages = (total_items // page_size) + (1 if total_items % page_size > 0 else 0)
+ if offset > 9223372036854775807: # max value of bigint
+ return "The page number is too big.", HTTPStatus.BAD_REQUEST
+ portals = query.offset(offset).limit(page_size).all()
+ return {
+ "portals": [portal.to_dict(with_owner=True) for portal in portals],
+ "pagination": {
+ "page": page,
+ "pageSize": page_size,
+ "totalItems": total_items,
+ "totalPages": total_pages,
+ },
+ }
+
+
+@validate_uuid
+def admin_update_portal(id_, portal=None, body=None, token_info=None, user=None):
+ """Update a portal
+
+ :param id: ID of portal
+ :type id: str
+ :param body: Portal
+ :type body: dict | bytes
+
+ :rtype: Portal
+ """
+ check_user_is_admin(user)
+ if not connexion.request.is_json:
+ return RESPONSE_JSON_REQ
+ if not is_uuid(id_):
+ id_ = convert_objectid_to_uuid(id_)
+
+ if portal := Portal.query.get(id_):
+ # Grab the fields from the request
+ portal_dict = connexion.request.get_json()
+
+ # If the "owner" field is set, ignore it
+ portal_dict.pop("owner", None)
+
+ # update the portal info
+ portal.update(portal_dict)
+ session.add(portal)
+ session.commit()
+ return portal.to_dict()
+ else:
+ abort(HTTPStatus.NOT_FOUND)
+
+
+@validate_uuid
+def admin_delete_portal(id_, token_info=None, user=None):
+ """Delete a single portal"""
+ check_user_is_admin(user)
+ if not is_uuid(id_):
+ return f"Portal ID {id_} is not in UUID format", HTTPStatus.BAD_REQUEST
+ if portal := Portal.query.get(id_):
+ session.delete(portal)
+ session.commit()
+ return HTTPStatus.OK.phrase, HTTPStatus.OK
+ else:
+ abort(HTTPStatus.NOT_FOUND)
diff --git a/backend/ibutsu_server/controllers/admin/project_controller.py b/backend/ibutsu_server/controllers/admin/project_controller.py
index dad5d6a2..ce1d5b8f 100644
--- a/backend/ibutsu_server/controllers/admin/project_controller.py
+++ b/backend/ibutsu_server/controllers/admin/project_controller.py
@@ -51,12 +51,11 @@ def admin_get_project(id_, token_info=None, user=None):
:rtype: Project
"""
check_user_is_admin(user)
- project = Project.query.get(id_)
- if not project:
- project = Project.query.filter(Project.name == id_).first()
- if not project:
+
+ if project := Project.query.get(id_) or Project.query.filter(Project.name == id_).first():
+ return project.to_dict(with_owner=True)
+ else:
abort(HTTPStatus.NOT_FOUND)
- return project.to_dict(with_owner=True)
def admin_get_project_list(
@@ -127,35 +126,34 @@ def admin_update_project(id_, project=None, body=None, token_info=None, user=Non
return RESPONSE_JSON_REQ
if not is_uuid(id_):
id_ = convert_objectid_to_uuid(id_)
- project = Project.query.get(id_)
- if not project:
+ if project := Project.query.get(id_):
+ # Grab the fields from the request
+ project_dict = connexion.request.get_json()
+
+ # If the "owner" field is set, ignore it
+ project_dict.pop("owner", None)
+
+ # handle updating users separately
+ for username in project_dict.pop("users", []):
+ user_to_add = User.query.filter_by(email=username).first()
+ if user_to_add and user_to_add not in project.users:
+ project.users.append(user_to_add)
+
+ # Make sure the project owner is in the list of users
+ if project_dict.get("owner_id"):
+ owner = User.query.get(project_dict["owner_id"])
+ if owner and owner not in project.users:
+ project.users.append(owner)
+
+ # update the rest of the project info
+ project.update(project_dict)
+ session.add(project)
+ session.commit()
+ return project.to_dict()
+ else:
abort(HTTPStatus.NOT_FOUND)
- # Grab the fields from the request
- project_dict = connexion.request.get_json()
-
- # If the "owner" field is set, ignore it
- project_dict.pop("owner", None)
-
- # handle updating users separately
- for username in project_dict.pop("users", []):
- user_to_add = User.query.filter_by(email=username).first()
- if user_to_add and user_to_add not in project.users:
- project.users.append(user_to_add)
-
- # Make sure the project owner is in the list of users
- if project_dict.get("owner_id"):
- owner = User.query.get(project_dict["owner_id"])
- if owner and owner not in project.users:
- project.users.append(owner)
-
- # update the rest of the project info
- project.update(project_dict)
- session.add(project)
- session.commit()
- return project.to_dict()
-
@validate_uuid
def admin_delete_project(id_, token_info=None, user=None):
@@ -163,9 +161,10 @@ def admin_delete_project(id_, token_info=None, user=None):
check_user_is_admin(user)
if not is_uuid(id_):
return f"Project ID {id_} is not in UUID format", HTTPStatus.BAD_REQUEST
- project = Project.query.get(id_)
- if not project:
+
+ if project := Project.query.get(id_):
+ session.delete(project)
+ session.commit()
+ return HTTPStatus.OK.phrase, HTTPStatus.OK
+ else:
abort(HTTPStatus.NOT_FOUND)
- session.delete(project)
- session.commit()
- return HTTPStatus.OK.phrase, HTTPStatus.OK
diff --git a/backend/ibutsu_server/controllers/dashboard_controller.py b/backend/ibutsu_server/controllers/dashboard_controller.py
index 24037752..9ed0f4c7 100644
--- a/backend/ibutsu_server/controllers/dashboard_controller.py
+++ b/backend/ibutsu_server/controllers/dashboard_controller.py
@@ -11,7 +11,7 @@
from ibutsu_server.util.uuid import validate_uuid
-def add_dashboard(dashboard=None, token_info=None, user=None):
+def add_dashboard(dashboard=None, token_info=None, user=None) -> tuple[dict, int]:
"""Create a dashboard
:param body: Dashboard
@@ -22,17 +22,30 @@ def add_dashboard(dashboard=None, token_info=None, user=None):
if not connexion.request.is_json:
return RESPONSE_JSON_REQ
dashboard = Dashboard.from_dict(**connexion.request.get_json())
+
+ if dashboard.portal_id and dashboard.project_id:
+ return "Dashboard can only have one of project_id or portal_id", HTTPStatus.BAD_REQUEST
+
+ if not (dashboard.portal_id or dashboard.project_id):
+ return "Dashboard needs either project_id or portal_id", HTTPStatus.BAD_REQUEST
+
if dashboard.project_id and not project_has_user(dashboard.project_id, user):
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
+
+ # TODO utility function to resolve all users with at least one project set
+ # compare to projects assigned to the portal? or portals are open to all projects
+ # otherwise, limit to admin users or new portal_admin permission
+
if dashboard.user_id and not User.query.get(dashboard.user_id):
return f"User with ID {dashboard.user_id} doesn't exist", HTTPStatus.BAD_REQUEST
+
session.add(dashboard)
session.commit()
return dashboard.to_dict(), HTTPStatus.CREATED
@validate_uuid
-def get_dashboard(id_, token_info=None, user=None):
+def get_dashboard(id_, token_info=None, user=None) -> dict:
"""Get a single dashboard by ID
:param id: ID of test dashboard
@@ -45,16 +58,19 @@ def get_dashboard(id_, token_info=None, user=None):
return "Dashboard not found", HTTPStatus.NOT_FOUND
if dashboard and dashboard.project and not project_has_user(dashboard.project, user):
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
+ # TODO test against dashboard with only portal set
return dashboard.to_dict()
def get_dashboard_list(
- filter_=None, project_id=None, page=1, page_size=25, token_info=None, user=None
-):
+ filter_=None, project_id=None, portal_id=None, page=1, page_size=25, token_info=None, user=None
+) -> dict[list[dict], dict]:
"""Get a list of dashboards
:param project_id: Filter dashboards by project ID
:type project_id: str
+ :param portal_id: Filter dashboards by portal ID
+ :type portal_id: str
:param user_id: Filter dashboards by user ID
:type user_id: str
:param limit: Limit the dashboards
@@ -66,6 +82,11 @@ def get_dashboard_list(
"""
query = Dashboard.query
project = None
+ portal = None
+ if portal_id is not None and project_id is not None:
+ return "Dashboard list can only have one of project_id or portal_id", HTTPStatus.BAD_REQUEST
+
+ # Project filter injection
if "project_id" in connexion.request.args:
project = Project.query.get(connexion.request.args["project_id"])
if project:
@@ -73,6 +94,13 @@ def get_dashboard_list(
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
query = query.filter(Dashboard.project_id == project_id)
+ # Portal filter injection
+ if "portal_id" in connexion.request.args:
+ portal = Project.query.get(connexion.request.args["portal_id"])
+ if portal:
+ query = query.filter(Dashboard.portal_id == portal_id)
+
+ # Other filters follow
if filter_:
for filter_string in filter_:
filter_clause = convert_filter(filter_string, Dashboard)
@@ -96,10 +124,10 @@ def get_dashboard_list(
@validate_uuid
-def update_dashboard(id_, dashboard=None, token_info=None, user=None):
+def update_dashboard(id_, dashboard=None, token_info=None, user=None) -> dict:
"""Update a dashboard
- :param id: ID of test dashboard
+ :param id: ID of dashboard
:type id: str
:param body: Dashboard
:type body: dict | bytes
@@ -113,11 +141,17 @@ def update_dashboard(id_, dashboard=None, token_info=None, user=None):
dashboard_dict["metadata"]["project"], user
):
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
+
+ # TODO user/admin check for portal ref
+
dashboard = Dashboard.query.get(id_)
if not dashboard:
return "Dashboard not found", HTTPStatus.NOT_FOUND
- if project_has_user(dashboard.project, user):
+ if dashboard.project_id is not None and project_has_user(dashboard.project, user):
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
+
+ # TODO user/admin check for portal ref
+
dashboard.update(connexion.request.get_json())
session.add(dashboard)
session.commit()
@@ -125,7 +159,7 @@ def update_dashboard(id_, dashboard=None, token_info=None, user=None):
@validate_uuid
-def delete_dashboard(id_, token_info=None, user=None):
+def delete_dashboard(id_, token_info=None, user=None) -> tuple[str, int]:
"""Deletes a dashboard
:param id: ID of the dashboard to delete
@@ -136,8 +170,9 @@ def delete_dashboard(id_, token_info=None, user=None):
dashboard = Dashboard.query.get(id_)
if not dashboard:
return HTTPStatus.NOT_FOUND.phrase, HTTPStatus.NOT_FOUND
- if not project_has_user(dashboard.project, user):
+ if dashboard.project_id is not None and not project_has_user(dashboard.project, user):
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
+ # TODO user/admin check for portal ref
widget_configs = WidgetConfig.query.filter(WidgetConfig.dashboard_id == dashboard.id).all()
for widget_config in widget_configs:
session.delete(widget_config)
diff --git a/backend/ibutsu_server/controllers/portal_controller.py b/backend/ibutsu_server/controllers/portal_controller.py
new file mode 100644
index 00000000..15aa68d4
--- /dev/null
+++ b/backend/ibutsu_server/controllers/portal_controller.py
@@ -0,0 +1,129 @@
+from http import HTTPStatus
+
+import connexion
+
+from ibutsu_server.constants import RESPONSE_JSON_REQ
+from ibutsu_server.db.base import session
+from ibutsu_server.db.models import Portal, User
+from ibutsu_server.filters import convert_filter
+from ibutsu_server.util.query import get_offset
+from ibutsu_server.util.uuid import convert_objectid_to_uuid, is_uuid, validate_uuid
+
+
+def add_portal(portal=None, token_info=None, user=None) -> dict:
+ """Create a portal
+
+ :param body: Portal
+ :type body: dict | bytes
+
+ :rtype: Portal
+ """
+ if not connexion.request.is_json:
+ return RESPONSE_JSON_REQ
+ portal = Portal.from_dict(**connexion.request.get_json())
+ # check if portal already exists
+ if portal.id and Portal.query.get(portal.id):
+ return f"Portal id {portal.id} already exists.", HTTPStatus.CONFLICT
+ user = User.query.get(user)
+ if user:
+ portal.owner = user
+ session.add(portal)
+ session.commit()
+ return portal.to_dict(), HTTPStatus.CREATED
+
+
+@validate_uuid
+def get_portal(id_, token_info=None, user=None) -> dict:
+ """Get a single portal by ID
+
+ :param id: ID of test portal
+ :type id: str
+
+ :rtype: Portal
+ """
+ if not is_uuid(id_):
+ id_ = convert_objectid_to_uuid(id_)
+ portal = Portal.query.filter(Portal.name == id_).first()
+ if not portal:
+ portal = Portal.query.get(id_)
+ # any user can get portals
+ if not portal:
+ return "Portal not found", HTTPStatus.NOT_FOUND
+ return portal.to_dict()
+
+
+def get_portal_list(
+ filter_=None,
+ owner_id=None,
+ page=1,
+ page_size=25,
+ token_info=None,
+ user=None,
+) -> dict[list[dict], dict]:
+ """Get a list of portals
+
+ :param owner_id: Filter portals by owner ID
+ :type owner_id: str
+ :param limit: Limit the portals
+ :type limit: int
+ :param offset: Offset the portals
+ :type offset: int
+
+ :rtype: List[Portal]
+ """
+ # TODO evaluate and test this filter need
+ query = Portal.query
+ if owner_id:
+ query = query.filter(Portal.owner_id == owner_id)
+
+ if filter_:
+ for filter_string in filter_:
+ filter_clause = convert_filter(filter_string, Portal)
+ if filter_clause is not None:
+ query = query.filter(filter_clause)
+
+ offset = get_offset(page, page_size)
+ total_items = query.count()
+ total_pages = (total_items // page_size) + (1 if total_items % page_size > 0 else 0)
+ portals = query.offset(offset).limit(page_size).all()
+ return {
+ "portals": [portal.to_dict() for portal in portals],
+ "pagination": {
+ "page": page,
+ "pageSize": page_size,
+ "totalItems": total_items,
+ "totalPages": total_pages,
+ },
+ }
+
+
+@validate_uuid
+def update_portal(id_, portal=None, token_info=None, user=None, **kwargs) -> dict:
+ """Update a portal
+
+ :param id: ID of portal
+ :type id: str
+ :param body: Portal
+ :type body: dict | bytes
+
+ :rtype: Portal
+ """
+ if not connexion.request.is_json:
+ return RESPONSE_JSON_REQ
+ if not is_uuid(id_):
+ id_ = convert_objectid_to_uuid(id_)
+ portal = Portal.query.get(id_)
+
+ if not portal:
+ return "Portal not found", HTTPStatus.NOT_FOUND
+
+ user = User.query.get(user)
+ if not user.is_superadmin and (not portal.owner or portal.owner.id != user.id):
+ return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
+
+ # update the portal info
+ updates = connexion.request.get_json()
+ portal.update(updates)
+ session.add(portal)
+ session.commit()
+ return portal.to_dict()
diff --git a/backend/ibutsu_server/controllers/widget_config_controller.py b/backend/ibutsu_server/controllers/widget_config_controller.py
index 53a3c1ff..2609ea59 100644
--- a/backend/ibutsu_server/controllers/widget_config_controller.py
+++ b/backend/ibutsu_server/controllers/widget_config_controller.py
@@ -7,10 +7,13 @@
from ibutsu_server.db.base import session
from ibutsu_server.db.models import WidgetConfig
from ibutsu_server.filters import convert_filter
+from ibutsu_server.util.portals import get_portal
from ibutsu_server.util.projects import get_project, project_has_user
from ibutsu_server.util.query import get_offset
from ibutsu_server.util.uuid import validate_uuid
+# TODO: pydantic validation of request data structure
+
def add_widget_config(widget_config=None, token_info=None, user=None):
"""Create a new widget config
@@ -23,22 +26,46 @@ def add_widget_config(widget_config=None, token_info=None, user=None):
if not connexion.request.is_json:
return RESPONSE_JSON_REQ
data = connexion.request.json
+
+ # bad request checks
if data["widget"] not in WIDGET_TYPES.keys():
return "Bad request, widget type does not exist", HTTPStatus.BAD_REQUEST
+ # TODO: openapi plugins to support exclusive options at the schema level
+ if (
+ not any(data.get(key) for key in ["portal_id", "portal", "project_id", "project"])
+ or (data.get("project_id") or data.get("project"))
+ and (data.get("portal_id") or data.get("portal"))
+ ):
+ return (
+ "Bad request, widget config requires one of project or portal",
+ HTTPStatus.BAD_REQUEST,
+ )
+
# add default weight of 10
- if not data.get("weight"):
- data["weight"] = 10
- # Look up the project id
+ data["weight"] = data.get("weight", 10)
+
+ # project relationship
+ # TODO BAD_REQUEST when the project doesn't resolve
if data.get("project"):
project = get_project(data.pop("project"))
if not project_has_user(project, user):
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
data["project_id"] = project.id
+
+ # portal relationship
+ # TODO BAD_REQUEST when the portal doesn't resolve
+ if data.get("portal"):
+ portal = get_portal(data.pop("portal"))
+ # TODO portal user/admin check
+ data["portal_id"] = portal.id
+
# default to make views navigable
if data.get("navigable") and isinstance(data["navigable"], str):
data["navigable"] = data["navigable"][0] in ALLOWED_TRUE_BOOLEANS
if data.get("type") == "view" and data.get("navigable") is None:
data["navigable"] = True
+
+ # commit the classmethod constructed model
widget_config = WidgetConfig.from_dict(**data)
session.add(widget_config)
session.commit()
@@ -80,6 +107,10 @@ def get_widget_config_list(filter_=None, page=1, page_size=25):
WidgetConfig.project_id.is_(None),
convert_filter(filter_string, WidgetConfig),
)
+ elif "portal" in filter_string:
+ filter_clause = or_(
+ WidgetConfig.portal_id.is_(None), convert_filter(filter_string, WidgetConfig)
+ )
else:
filter_clause = convert_filter(filter_string, WidgetConfig)
if filter_clause is not None:
@@ -113,20 +144,39 @@ def update_widget_config(id_, body=None, widget_config=None, token_info=None, us
if not connexion.request.is_json:
return RESPONSE_JSON_REQ
data = connexion.request.get_json()
+
+ # Bad request checks
if data.get("widget") and data["widget"] not in WIDGET_TYPES.keys():
return "Bad request, widget type does not exist", HTTPStatus.BAD_REQUEST
- # Look up the project id
+ # check portal and project fields if any of them are set
+ # not required on an update, existing entity should already have it.
+ if any(data.get(key) for key in ["portal_id", "portal", "project_id", "project"]):
+ if (data.get("project_id") or data.get("project")) and (
+ data.get("portal_id") or data.get("portal")
+ ):
+ return (
+ "Bad request, widget config requires one of project or portal",
+ HTTPStatus.BAD_REQUEST,
+ )
+
+ # project relationship
if data.get("project"):
project = get_project(data.pop("project"))
if not project_has_user(project, user):
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
data["project_id"] = project.id
+
+ # portal relationship
+ if data.get("portal"):
+ portal = get_portal(data.pop("portal"))
+ # TODO portal user/admin check
+ data["portal_id"] = portal.id
+
widget_config = WidgetConfig.query.get(id_)
if not widget_config:
return "Widget config not found", HTTPStatus.NOT_FOUND
# add default weight of 10
- if not widget_config.weight:
- widget_config.weight = 10
+ widget_config.weight = getattr(widget_config, "weight", None) or 10
# default to make views navigable
if data.get("navigable") and isinstance(data["navigable"], str):
data["navigable"] = data["navigable"][0] in ALLOWED_TRUE_BOOLEANS
@@ -153,6 +203,7 @@ def delete_widget_config(id_, token_info=None, user=None):
else:
if widget_config.project and not project_has_user(widget_config.project, user):
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
+ # TODO portal user/admin check
session.delete(widget_config)
session.commit()
return HTTPStatus.OK.phrase, HTTPStatus.OK
diff --git a/backend/ibutsu_server/db/models.py b/backend/ibutsu_server/db/models.py
index 1b5e400c..3bf075d3 100644
--- a/backend/ibutsu_server/db/models.py
+++ b/backend/ibutsu_server/db/models.py
@@ -94,10 +94,12 @@ class Dashboard(Model, ModelMixin):
title = Column(Text, index=True)
description = Column(Text, default="")
filters = Column(Text, default="")
- project_id = Column(PortableUUID(), ForeignKey("projects.id"), index=True)
+ portal_id = Column(PortableUUID(), ForeignKey("portals.id"), index=True, nullable=True)
+ project_id = Column(PortableUUID(), ForeignKey("projects.id"), index=True, nullable=True)
user_id = Column(PortableUUID(), ForeignKey("users.id"), index=True)
widgets = relationship("WidgetConfig")
project = relationship("Project", back_populates="dashboards", foreign_keys=[project_id])
+ portal = relationship("Portal", back_populates="dashboards", foreign_keys=[portal_id])
class Group(Model, ModelMixin):
@@ -147,6 +149,30 @@ def to_dict(self, with_owner=False):
return project_dict
+class Portal(Model, ModelMixin):
+ # TODO: Consider common mixin for overlap between project and portal
+ __tablename__ = "portals"
+ name = Column(Text, index=True)
+ title = Column(Text, index=True)
+ owner_id = Column(PortableUUID(), ForeignKey("users.id"), index=True)
+ # group_id = Column(PortableUUID(), ForeignKey("groups.id"), index=True)
+ default_dashboard_id = Column(PortableUUID(), ForeignKey("dashboards.id"))
+ default_dashboard = relationship("Dashboard", foreign_keys=[default_dashboard_id])
+ dashboards = relationship(
+ "Dashboard", back_populates="portal", foreign_keys=[Dashboard.portal_id]
+ )
+ widget_configs = relationship("WidgetConfig", back_populates="portal")
+
+ def to_dict(self, with_owner=False):
+ """An overridden method to include the owner"""
+ portal_dict = super().to_dict()
+ if with_owner and self.owner:
+ portal_dict["owner"] = self.owner.to_dict()
+ if self.default_dashboard:
+ portal_dict["defaultDashboard"] = self.default_dashboard.to_dict()
+ return portal_dict
+
+
class Report(Model, ModelMixin):
__tablename__ = "reports"
created = Column(DateTime, default=datetime.utcnow, index=True)
@@ -208,7 +234,8 @@ class WidgetConfig(Model, ModelMixin):
__tablename__ = "widget_configs"
navigable = Column(Boolean, index=True)
params = Column(mutable_json_type(dbtype=PortableJSON()))
- project_id = Column(PortableUUID(), ForeignKey("projects.id"), index=True)
+ portal_id = Column(PortableUUID(), ForeignKey("portals.id"), index=True, nullable=True)
+ project_id = Column(PortableUUID(), ForeignKey("projects.id"), index=True, nullable=True)
dashboard_id = Column(PortableUUID(), ForeignKey("dashboards.id"), index=True)
title = Column(Text, index=True)
type = Column(Text, index=True)
@@ -216,6 +243,7 @@ class WidgetConfig(Model, ModelMixin):
widget = Column(Text, index=True)
project = relationship("Project", back_populates="widget_configs")
+ portal = relationship("Portal", back_populates="widget_configs")
class User(Model, ModelMixin):
@@ -229,6 +257,7 @@ class User(Model, ModelMixin):
group_id = Column(PortableUUID(), ForeignKey("groups.id"), index=True)
dashboards = relationship("Dashboard")
owned_projects = relationship("Project", backref="owner")
+ owned_portals = relationship("Portal", backref="owner")
tokens = relationship("Token", backref="user")
projects = relationship(
"Project", secondary=users_projects, backref=backref("users", lazy="subquery")
diff --git a/backend/ibutsu_server/db/upgrades.py b/backend/ibutsu_server/db/upgrades.py
index e7a6a2c7..20a38516 100644
--- a/backend/ibutsu_server/db/upgrades.py
+++ b/backend/ibutsu_server/db/upgrades.py
@@ -7,7 +7,7 @@
from ibutsu_server.db.base import Boolean, Column, ForeignKey, Text
from ibutsu_server.db.types import PortableUUID
-__version__ = 5
+__version__ = 6
def get_upgrade_op(session):
@@ -175,3 +175,78 @@ def upgrade_5(session):
"projects",
Column("default_dashboard_id", PortableUUID(), ForeignKey("dashboards.id")),
)
+
+
+def upgrade_6(session):
+ """Version 6 upgrade
+
+ This upgrade adds portals relationships
+ """
+ engine = session.get_bind()
+ op = get_upgrade_op(session)
+ metadata = MetaData()
+ metadata.reflect(bind=engine)
+
+ # WidgetConfig model changes
+ wc_table = metadata.tables.get("widget_configs")
+ wc_table_present = bool("widget_configs" in metadata.tables and wc_table is not None)
+
+ # widgetconfig new column portal_id
+ if wc_table_present and "portal_id" not in [col.name for col in wc_table.columns]:
+ op.add_column(
+ "widget_configs",
+ Column(
+ "portal_id", PortableUUID(), ForeignKey("portals.id"), nullable=True, index=True
+ ),
+ )
+ if engine.url.get_dialect().name != "sqlite":
+ # SQLite doesn't support ALTER TABLE ADD CONSTRAINT
+ op.create_foreign_key(
+ "fk_widget_configs_portal_id",
+ "widget_configs",
+ "portals",
+ ["portal_id"],
+ ["id"],
+ )
+
+ # widgetconfig alter project_id -> nullable
+ # can't alter tables in unittest with sqllite
+ # TODO replace sqlite for unit tests
+ if (
+ engine.url.get_dialect().name != "sqlite"
+ and wc_table_present
+ and "project_id" in [col.name for col in wc_table.columns]
+ ):
+ op.alter_column("widget_configs", "project_id", nullable=True)
+
+ # Dashboard model changes
+ dash_table = metadata.tables.get("dashboards")
+ dash_table_present = bool("dashboards" in metadata.tables and dash_table is not None)
+
+ # dashboard alter project_id -> nullable
+ # can't alter tables in unittest with sqllite
+ # TODO replace sqlite for unit tests
+ if (
+ engine.url.get_dialect().name != "sqlite"
+ and dash_table_present
+ and "project_id" in [col.name for col in dash_table.columns]
+ ):
+ op.alter_column("dashboards", "project_id", nullable=True)
+
+ # dashboard new column portal_id
+ if dash_table_present and "portal_id" not in [col.name for col in dash_table.columns]:
+ op.add_column(
+ "dashboards",
+ Column(
+ "portal_id", PortableUUID(), ForeignKey("portals.id"), nullable=True, index=True
+ ),
+ )
+ if engine.url.get_dialect().name != "sqlite":
+ # SQLite doesn't support ALTER TABLE ADD CONSTRAINT
+ op.create_foreign_key(
+ "fk_dashboards_portal_id",
+ "dashboards",
+ "portals",
+ ["portal_id"],
+ ["id"],
+ )
diff --git a/backend/ibutsu_server/openapi/openapi.yaml b/backend/ibutsu_server/openapi/openapi.yaml
index 5b9a3507..273b7583 100644
--- a/backend/ibutsu_server/openapi/openapi.yaml
+++ b/backend/ibutsu_server/openapi/openapi.yaml
@@ -14,6 +14,8 @@ tags:
name: run
- description: A collection of test runs
name: project
+ - description: Dashboard aggregation of results from multiple projects
+ name: portal
- description: A group of projects
name: group
- description: A report
@@ -583,6 +585,123 @@ paths:
tags:
- run
x-openapi-router-controller: ibutsu_server.controllers.run_controller
+ /portal:
+ get:
+ operationId: get_portal_list
+ parameters:
+ - description: Fields to filter by
+ explode: true
+ in: query
+ name: filter
+ required: false
+ schema:
+ items:
+ type: string
+ type: array
+ style: form
+ - description: Filter portals by owner ID
+ explode: true
+ in: query
+ name: ownerId
+ required: false
+ schema:
+ type: string
+ style: form
+ - $ref: '#/components/parameters/Page'
+ - $ref: '#/components/parameters/PageSize'
+ responses:
+ 200:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PortalList'
+ description: Array of portals
+ summary: Get a list of portals
+ tags:
+ - portal
+ x-openapi-router-controller: ibutsu_server.controllers.portal_controller
+ post:
+ operationId: add_portal
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
+ description: Portal, a home for multi project dashboards
+ required: true
+ responses:
+ 201:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
+ description: A portal was created
+ 400:
+ description: Bad request, JSON required
+ 409:
+ description: Bad request, portal ID already exists
+ summary: Create a portal
+ tags:
+ - portal
+ x-openapi-router-controller: ibutsu_server.controllers.portal_controller
+ /portal/{id}:
+ get:
+ operationId: get_portal
+ parameters:
+ - description: ID of portal to get
+ explode: false
+ in: path
+ name: id
+ required: true
+ schema:
+ type: string
+ style: simple
+ responses:
+ 200:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
+ description: Portal object
+ 404:
+ description: Portal not found
+ summary: Get a single portal by ID
+ tags:
+ - portal
+ x-openapi-router-controller: ibutsu_server.controllers.portal_controller
+ put:
+ operationId: update_portal
+ parameters:
+ - description: ID of portal to modify
+ explode: false
+ in: path
+ name: id
+ required: true
+ schema:
+ type: string
+ format: uuid
+ style: simple
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
+ description: Portal
+ responses:
+ 200:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
+ description: Portal object
+ 400:
+ description: Bad request, JSON required or not enough parameters
+ 404:
+ description: Portal not found
+ summary: Update a Portal
+ tags:
+ - portal
+ x-openapi-router-controller: ibutsu_server.controllers.portal_controller
/project:
get:
operationId: get_project_list
@@ -1298,6 +1417,9 @@ paths:
tags:
- widget-config
x-openapi-router-controller: ibutsu_server.controllers.widget_config_controller
+ # https://github.com/OpenAPITools/openapi-generator/issues/8722
+ # x-dependencies:
+ # - OnlyOne(portal_id, project_id);
/widget-config/{id}:
get:
operationId: get_widget_config
@@ -2086,6 +2208,152 @@ paths:
tags:
- admin/project management
x-openapi-router-controller: ibutsu_server.controllers.admin.project_controller
+ /admin/portal:
+ get:
+ operationId: admin_get_portal_list
+ parameters:
+ - description: Fields to filter by
+ explode: true
+ in: query
+ name: filter
+ required: false
+ schema:
+ items:
+ type: string
+ type: array
+ style: form
+ - $ref: '#/components/parameters/Page'
+ - $ref: '#/components/parameters/PageSize'
+ responses:
+ 200:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PortalList'
+ description: Returns a list of portals
+ 401:
+ description: The user needs to be logged in
+ 403:
+ description: The user needs to be a superadmin
+ tags:
+ - admin/portal management
+ summary: Administration endpoint to return a list of portals. Only accessible to superadmins.
+ x-openapi-router-controller: ibutsu_server.controllers.admin.portal_controller
+ post:
+ operationId: admin_add_portal
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
+ description: A portal
+ responses:
+ 201:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
+ description: A portal was created
+ 400:
+ description: Bad request, JSON required
+ 401:
+ description: The user needs to be logged in
+ 403:
+ description: The user needs to be a superadmin
+ tags:
+ - admin/portal management
+ summary: Administration endpoint to manually add a portal. Only accessible to superadmins.
+ x-openapi-router-controller: ibutsu_server.controllers.admin.portal_controller
+ /admin/portal/{id}:
+ get:
+ operationId: admin_get_portal
+ parameters:
+ - description: The id of a portal
+ explode: false
+ in: path
+ name: id
+ required: true
+ schema:
+ type: string
+ format: uuid
+ style: simple
+ responses:
+ 200:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
+ description: Returns a portal
+ 401:
+ description: The user needs to be logged in
+ 403:
+ description: The user needs to be a superadmin
+ 404:
+ description: The portal does not exist
+ tags:
+ - admin/portal management
+ summary: Administration endpoint to return a portal. Only accessible to superadmins.
+ x-openapi-router-controller: ibutsu_server.controllers.admin.portal_controller
+ put:
+ operationId: admin_update_portal
+ parameters:
+ - description: The ID of the portal to update
+ explode: false
+ in: path
+ name: id
+ required: true
+ schema:
+ type: string
+ format: uuid
+ style: simple
+ requestBody:
+ $ref: '#/components/requestBodies/Portal'
+ responses:
+ 200:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
+ description: successful operation
+ 400:
+ description: Bad reqest, JSON required or not enough parameters
+ 401:
+ description: The user needs to be logged in
+ 403:
+ description: The user needs to be a superadmin
+ 404:
+ description: Portal not found
+ summary: Administration endpoint to update a portal. Only accessible to superadmins.
+ tags:
+ - admin/portal management
+ x-openapi-router-controller: ibutsu_server.controllers.admin.portal_controller
+ delete:
+ operationId: admin_delete_portal
+ parameters:
+ - description: The ID of the portal to delete
+ explode: false
+ in: path
+ name: id
+ required: true
+ schema:
+ type: string
+ format: uuid
+ style: simple
+ responses:
+ 200:
+ description: The specified portal was deleted
+ 401:
+ description: The user needs to be logged in
+ 403:
+ description: The user needs to be a superadmin
+ 404:
+ description: Portal not found
+ summary: Administration endpoint to delete a portal. Only accessible to superadmins.
+ tags:
+ - admin/portal management
+ x-openapi-router-controller: ibutsu_server.controllers.admin.portal_controller
+
+
components:
requestBodies:
Result:
@@ -2185,6 +2453,11 @@ components:
application/json:
schema:
$ref: '#/components/schemas/Project'
+ Portal:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Portal'
schemas:
Result:
example:
@@ -2388,6 +2661,33 @@ components:
description: The date this artifact was uploaded
type: string
type: object
+ Portal:
+ example:
+ id: fe5ad2dc-330a-11ef-9036-12b95372ee33
+ name: my-portal
+ title: My portal
+ owner_id: 07a776ba-330b-11ef-9338-12b95372ee33
+ properties:
+ id:
+ description: Unique ID of the portal
+ example: fe5ad2dc-330a-11ef-9036-12b95372ee33
+ type: string
+ format: uuid
+ name:
+ description: The machine name of the portal
+ example: my-portal
+ type: string
+ title:
+ description: The human-readable title of the portal
+ example: My portal
+ type: string
+ owner_id:
+ description: The ID of the owner of this portal
+ example: 07a776ba-330b-11ef-9338-12b95372ee33
+ type: string
+ format: uuid
+ nullable: true
+ type: object
Project:
example:
id: 44941c55-9736-42f6-acce-ca3c4739d0f3
@@ -2573,11 +2873,13 @@ components:
format: uuid
type: object
WidgetConfig:
+ # TODO schema doesn't support examples keyword, show with portal and with project
example:
id: afbcf5c7-1ffd-4367-b228-5a868c29e0ef
type: widget
widget: jenkins-heatmap
project_id: 44941c55-9736-42f6-acce-ca3c4739d0f3
+ portal_id: null
weight: 0
params:
job_name: integration_tests
@@ -2600,10 +2902,17 @@ components:
example: jenkins-heatmap
type: string
project_id:
- description: The project ID for which the widget is designed
+ description: The project ID for the widget, exclusive with portal_id
example: 44941c55-9736-42f6-acce-ca3c4739d0f3
type: string
format: uuid
+ nullable: true
+ portal_id:
+ description: The portal ID for the widget, exclusive with project_id
+ example: fe5ad2dc-330a-11ef-9036-12b95372ee33
+ type: string
+ format: uuid
+ nullable: true
weight:
description: The weighting for the widget, lower weight means it will display first
example: 0
@@ -3063,6 +3372,26 @@ components:
pagination:
$ref: '#/components/schemas/Pagination'
type: object
+ PortalList:
+ example:
+ portals:
+ - id: fe5ad2dc-330a-11ef-9036-12b95372ee33
+ name: My Portal
+ title: my-portal
+ ownerId: 07a776ba-330b-11ef-9338-12b95372ee33
+ pagination:
+ page: 2
+ pageSize: 25
+ totalPages: 10
+ totalItems: 243
+ properties:
+ projects:
+ items:
+ $ref: '#/components/schemas/Portal'
+ type: array
+ pagination:
+ $ref: '#/components/schemas/Pagination'
+ type: object
ProjectList:
example:
projects:
diff --git a/backend/ibutsu_server/test/__init__.py b/backend/ibutsu_server/test/__init__.py
index 9d1e24e3..1a398fac 100644
--- a/backend/ibutsu_server/test/__init__.py
+++ b/backend/ibutsu_server/test/__init__.py
@@ -34,6 +34,9 @@ def _wrapped(*args, **kwargs):
return decorate
+MESSAGE = "Response body is : %s"
+
+
class BaseTestCase(TestCase):
def create_app(self):
logging.getLogger("connexion.operation").setLevel("ERROR")
@@ -58,7 +61,17 @@ def create_app(self):
self.test_user = User(name="Test User", email="test@example.com", is_active=True)
session.add(self.test_user)
session.commit()
+
+ # store the jwt_token and standardized headers for use in tests
self.jwt_token = generate_token(self.test_user.id)
+ self.headers_no_content = {
+ "Accept": "application/json",
+ "Authorization": f"Bearer {self.jwt_token}",
+ }
+ self.headers = self.headers_no_content.copy()
+ self.headers.update({"Content-Type": "application/json"})
+
+ # create the login token and commit to session
token = Token(name="login-token", user=self.test_user, token=self.jwt_token)
session.add(token)
session.commit()
@@ -68,13 +81,26 @@ def create_app(self):
ibutsu_server.tasks.task = mock_task
return app
+ # Override these assert functions from TestCase so we can set a helpful default message
+ def assert_200(self, response, message=None):
+ """
+ Checks if response status code is 200
+ :param response: Flask response
+ :param message: Message to display on test failure
+ """
+ self.assert_status(
+ response, HTTPStatus.OK, message or MESSAGE.format(response.data.decode("utf-8"))
+ )
+
def assert_201(self, response, message=None):
"""
Checks if response status code is 201
:param response: Flask response
:param message: Message to display on test failure
"""
- self.assert_status(response, HTTPStatus.CREATED, message)
+ self.assert_status(
+ response, HTTPStatus.CREATED, message or MESSAGE.format(response.data.decode("utf-8"))
+ )
def assert_503(self, response, message=None):
"""
@@ -82,7 +108,43 @@ def assert_503(self, response, message=None):
:param response: Flask response
:param message: Message to display on test failure
"""
- self.assert_status(response, HTTPStatus.SERVICE_UNAVAILABLE, message)
+ self.assert_status(
+ response,
+ HTTPStatus.SERVICE_UNAVAILABLE,
+ message or MESSAGE.format(response.data.decode("utf-8")),
+ )
+
+ def assert_400(self, response, message=None):
+ """
+ Checks if response code is 400
+ :param response: Flask response
+ :param message: message to display on test failure
+ """
+ self.assert_status(
+ response,
+ HTTPStatus.BAD_REQUEST,
+ message or MESSAGE.format(response.data.decode("utf-8")),
+ )
+
+ def assert_404(self, response, message=None):
+ """
+ Checks if response code is 404
+ :param response: Flask response
+ :param message: message to display on test failure
+ """
+ self.assert_status(
+ response, HTTPStatus.NOT_FOUND, message or MESSAGE.format(response.data.decode("utf-8"))
+ )
+
+ def assert_409(self, response, message=None):
+ """
+ Checks if response code is 409
+ :param response: Flask response
+ :param message: message to display on test failure
+ """
+ self.assert_status(
+ response, HTTPStatus.CONFLICT, message or MESSAGE.format(response.data.decode("utf-8"))
+ )
def assert_equal(self, first, second, msg=None):
"""Alias"""
@@ -156,6 +218,10 @@ class MockProject(MockModel):
COLUMNS = ["id", "name", "title", "owner_id", "group_id", "users"]
+class MockPortal(MockModel):
+ COLUMNS = ["id", "name", "title", "owner_id", "default_dashboard_id"]
+
+
class MockResult(MockModel):
COLUMNS = [
"id",
@@ -207,6 +273,7 @@ class MockRun(MockModel):
class MockDashboard(MockModel):
+ # TODO: dashboard columns and unit test coverage
COLUMNS = []
@@ -215,12 +282,14 @@ class MockWidgetConfig(MockModel):
"id",
"navigable",
"params",
+ "portal_id",
"project_id",
"dashboard_id",
"title",
"type",
"weight",
"widget",
+ "portal",
"project",
"dashboard",
]
diff --git a/backend/ibutsu_server/test/test_artifact_controller.py b/backend/ibutsu_server/test/test_artifact_controller.py
index 0ea33f26..55ebfb41 100644
--- a/backend/ibutsu_server/test/test_artifact_controller.py
+++ b/backend/ibutsu_server/test/test_artifact_controller.py
@@ -59,46 +59,38 @@ def test_delete_artifact(self):
Delete an artifact
"""
- headers = {"Authorization": f"Bearer {self.jwt_token}"}
+ headers = {"Authorization": self.headers.get("Authorization")}
response = self.client.open(f"/api/artifact/{MOCK_ID}", method="DELETE", headers=headers)
self.mock_artifact.query.get.assert_called_once_with(MOCK_ID)
self.mock_session.delete.assert_called_once_with(MOCK_ARTIFACT)
self.mock_session.commit.assert_called_once()
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
def test_download_artifact(self):
"""Test case for download_artifact
Download an artifact
"""
- headers = {
- "Accept": "application/octet-stream",
- "Authorization": f"Bearer {self.jwt_token}",
- }
response = self.client.open(
f"/api/artifact/{MOCK_ID}/download",
method="GET",
- headers=headers,
+ headers=self.headers_no_content,
)
self.mock_artifact.query.get.assert_called_once_with(MOCK_ID)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
def test_get_artifact(self):
"""Test case for get_artifact
Get a single artifact
"""
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
response = self.client.open(
f"/api/artifact/{MOCK_ID}",
method="GET",
- headers=headers,
+ headers=self.headers_no_content,
)
self.mock_artifact.query.get.assert_called_once_with(MOCK_ID)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
def test_get_artifact_list(self):
"""Test case for get_artifact_list
@@ -106,15 +98,15 @@ def test_get_artifact_list(self):
Get a (filtered) list of artifacts
"""
query_string = [("resultId", MOCK_RESULT_ID), ("page", 56), ("pageSize", 56)]
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
- "/api/artifact", method="GET", headers=headers, query_string=query_string
+ "/api/artifact",
+ method="GET",
+ headers=self.headers_no_content,
+ query_string=query_string,
)
self.mock_limit.return_value.offset.return_value.all.assert_called_once()
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
@skip("Something is getting crossed in the validation layer")
def test_upload_artifact(self):
@@ -122,11 +114,12 @@ def test_upload_artifact(self):
Uploads a test run artifact
"""
- headers = {
- "Accept": "application/json",
- "Content-Type": "multipart/form-data",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+ headers = self.headers.copy()
+ headers.update(
+ {
+ "Content-Type": "multipart/form-data",
+ }
+ )
data = {
"resultId": MOCK_ID,
"filename": "log.txt",
@@ -140,4 +133,4 @@ def test_upload_artifact(self):
data=data,
content_type="multipart/form-data",
)
- self.assert_201(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_201(response)
diff --git a/backend/ibutsu_server/test/test_group_controller.py b/backend/ibutsu_server/test/test_group_controller.py
index 677a7589..955d40cc 100644
--- a/backend/ibutsu_server/test/test_group_controller.py
+++ b/backend/ibutsu_server/test/test_group_controller.py
@@ -36,16 +36,11 @@ def test_add_group(self):
Create a new group
"""
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
self.mock_group.query.get.return_value = None
response = self.client.open(
"/api/group",
method="POST",
- headers=headers,
+ headers=self.headers,
data=json.dumps({"name": "Example group"}),
content_type="application/json",
)
@@ -57,11 +52,9 @@ def test_get_group(self):
Get a group
"""
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open(f"/api/group/{MOCK_ID}", method="GET", headers=headers)
+ response = self.client.open(
+ f"/api/group/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
self.assert_equal(response.json, MOCK_GROUP_DICT)
@@ -71,12 +64,8 @@ def test_get_group_list(self):
Get a list of groups
"""
query_string = [("page", 56), ("pageSize", 56)]
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
response = self.client.open(
- "/api/group", method="GET", headers=headers, query_string=query_string
+ "/api/group", method="GET", headers=self.headers_no_content, query_string=query_string
)
self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
self.assert_equal(
@@ -97,15 +86,11 @@ def test_update_group(self):
Update a group
"""
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
f"/api/group/{MOCK_ID}",
method="PUT",
- headers=headers,
+ headers=self.headers,
data=json.dumps({"name": "Changed name"}),
content_type="application/json",
)
diff --git a/backend/ibutsu_server/test/test_health_controller.py b/backend/ibutsu_server/test/test_health_controller.py
index cd3b85ca..ce0e002f 100644
--- a/backend/ibutsu_server/test/test_health_controller.py
+++ b/backend/ibutsu_server/test/test_health_controller.py
@@ -11,24 +11,18 @@ def test_get_database_health(self):
Get a health report for the database
"""
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open("/api/health/database", method="GET", headers=headers)
- self.assert_503(response, "Response body is : " + response.data.decode("utf-8"))
+ response = self.client.open(
+ "/api/health/database", method="GET", headers=self.headers_no_content
+ )
+ self.assert_503(response)
def test_get_health(self):
"""Test case for get_health
Get a general health report
"""
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open("/api/health", method="GET", headers=headers)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ response = self.client.open("/api/health", method="GET", headers=self.headers_no_content)
+ self.assert_200(response)
if __name__ == "__main__":
diff --git a/backend/ibutsu_server/test/test_login_controller.py b/backend/ibutsu_server/test/test_login_controller.py
index af380172..dae8b7f2 100644
--- a/backend/ibutsu_server/test/test_login_controller.py
+++ b/backend/ibutsu_server/test/test_login_controller.py
@@ -28,6 +28,9 @@ def setUp(self):
self.mock_user = self.user_patcher.start()
self.mock_user.query.filter_by.return_value.first.return_value = MOCK_USER
+ self.headers_no_auth = self.headers.copy()
+ self.headers_no_auth.pop("Authorization")
+
def tearDown(self):
"""Teardown the mocks"""
self.user_patcher.stop()
@@ -46,15 +49,15 @@ def test_login(self, mocked_generate_token):
"email": MOCK_EMAIL,
"token": expected_token,
}
- headers = {"Accept": "application/json", "Content-Type": "application/json"}
+
response = self.client.open(
"/api/login",
method="POST",
- headers=headers,
+ headers=self.headers_no_auth,
data=json.dumps(login_details),
content_type="application/json",
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
assert response.json == expected_response
def test_login_empty_request(self):
@@ -69,15 +72,14 @@ def test_login_empty_request(self):
"title": HTTPStatus.BAD_REQUEST.phrase,
"type": "about:blank",
}
- headers = {"Accept": "application/json", "Content-Type": "application/json"}
response = self.client.open(
"/api/login",
method="POST",
- headers=headers,
+ headers=self.headers_no_auth,
data=json.dumps(login_details),
content_type="application/json",
)
- self.assert_400(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_400(response)
assert response.json == expected_response
def test_login_no_user(self):
@@ -90,16 +92,15 @@ def test_login_no_user(self):
"code": "INVALID",
"message": "Username and/or password are invalid",
}
- headers = {"Accept": "application/json", "Content-Type": "application/json"}
self.mock_user.query.filter_by.return_value.first.return_value = None
response = self.client.open(
"/api/login",
method="POST",
- headers=headers,
+ headers=self.headers_no_auth,
data=json.dumps(login_details),
content_type="application/json",
)
- self.assert_401(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_401(response)
assert response.json == expected_response
def test_login_bad_password(self):
@@ -112,15 +113,14 @@ def test_login_bad_password(self):
"code": "INVALID",
"message": "Username and/or password are invalid",
}
- headers = {"Accept": "application/json", "Content-Type": "application/json"}
response = self.client.open(
"/api/login",
method="POST",
- headers=headers,
+ headers=self.headers_no_auth,
data=json.dumps(login_details),
content_type="application/json",
)
- self.assert_401(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_401(response)
assert response.json == expected_response
def test_support(self):
@@ -133,14 +133,13 @@ def test_support(self):
"facebook": False,
"gitlab": True,
}
- headers = {"Accept": "application/json", "Content-Type": "application/json"}
response = self.client.open(
"/api/login/support",
method="GET",
- headers=headers,
+ headers=self.headers_no_auth,
content_type="application/json",
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
assert response.json == expected_response
def test_config_gitlab(self):
@@ -151,12 +150,11 @@ def test_config_gitlab(self):
"redirect_uri": f"http://{LOCALHOST}:8080/api/login/auth/gitlab",
"scope": "read_user",
}
- headers = {"Accept": "application/json", "Content-Type": "application/json"}
response = self.client.open(
"/api/login/config/gitlab",
method="GET",
- headers=headers,
+ headers=self.headers_no_auth,
content_type="application/json",
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
assert response.json == expected_response
diff --git a/backend/ibutsu_server/test/test_portal_controller.py b/backend/ibutsu_server/test/test_portal_controller.py
new file mode 100644
index 00000000..f6ee5818
--- /dev/null
+++ b/backend/ibutsu_server/test/test_portal_controller.py
@@ -0,0 +1,155 @@
+from unittest.mock import MagicMock, patch
+
+from flask import json
+
+from ibutsu_server.test import BaseTestCase, MockPortal, MockUser
+
+MOCK_ID = "f40991a6-3305-11ef-9083-12b95372ee33"
+MOCK_USER_ID = "0eea178e-3306-11ef-b969-12b95372ee33"
+
+MOCK_PORTAL = MockPortal(
+ id=MOCK_ID,
+ name="unittest-portal",
+ title="UnitTest Portal",
+ owner_id=MOCK_USER_ID,
+ default_dashboard_id="7e8a1684-3306-11ef-8433-12b95372ee33",
+)
+MOCK_USER = MockUser.from_dict(**{"id": MOCK_USER_ID})
+
+
+class TestPortalController(BaseTestCase):
+ """PortalController integration test stubs"""
+
+ def setUp(self):
+ """Set up a test data"""
+ MOCK_PORTAL.owner = self.test_user
+ self.session_patcher = patch("ibutsu_server.controllers.portal_controller.session")
+ self.mock_session = self.session_patcher.start()
+
+ mock_offset = MagicMock()
+ mock_offset.limit.return_value.all.return_value = [MOCK_PORTAL]
+
+ # mock portal return values for the DB query, and dict ctor
+ self.portal_patcher = patch("ibutsu_server.controllers.portal_controller.Portal")
+ self.mock_portal = self.portal_patcher.start()
+ self.mock_portal.query.get.return_value = MOCK_PORTAL
+ self.mock_portal.query.count.return_value = 1
+ self.mock_portal.from_dict.return_value = MOCK_PORTAL
+
+ # mock the offset for listing to return the only portal instance
+ self.mock_portal.query.offset.return_value = mock_offset
+
+ def tearDown(self):
+ """Teardown the mocks"""
+ self.portal_patcher.stop()
+ self.session_patcher.stop()
+
+ def test_add_portal(self):
+ """Test case for add_portal
+
+ Create a portal
+ """
+ self.user_patcher = patch("ibutsu_server.controllers.portal_controller.User")
+ self.mock_user = self.user_patcher.start()
+ self.mock_user.query.get.return_value = MOCK_USER
+ # clear the setUp mock for the query since we're adding it here
+ self.mock_portal.query.get.return_value = None
+
+ response = self.client.open(
+ "/api/portal",
+ method="POST",
+ headers=self.headers,
+ data=json.dumps(MOCK_PORTAL.to_dict()),
+ content_type="application/json",
+ )
+ self.assert_201(response)
+ self.assert_equal(response.json, MOCK_PORTAL.to_dict())
+ self.mock_session.add.assert_called_once_with(MOCK_PORTAL)
+ self.mock_session.commit.assert_called_once()
+ # teardown user patcher
+ self.user_patcher.stop()
+
+ def test_add_portal_409(self):
+ """Test case to hit a conflict on add"""
+ # MOCK_PORTAL is already there from setup, just try it again
+ response = self.client.open(
+ "/api/portal",
+ method="POST",
+ headers=self.headers,
+ data=json.dumps(MOCK_PORTAL.to_dict()),
+ content_type="application/json",
+ )
+ self.assert_409(response)
+
+ def test_get_portal_by_id(self):
+ """Test case for get_portal
+
+ Get a single portal by ID
+ """
+ self.mock_portal.query.filter.return_value.first.return_value = None
+
+ response = self.client.open(
+ f"/api/portal/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
+ self.assert_200(response)
+ self.assert_equal(response.json, MOCK_PORTAL.to_dict())
+ self.mock_portal.query.get.assert_called_once_with(MOCK_ID)
+
+ def test_get_portal_by_name(self):
+ """Test case for get_portal
+
+ Get a single portal by name
+ """
+ self.mock_portal.query.filter.return_value.first.return_value = MOCK_PORTAL
+
+ response = self.client.open(
+ f"/api/portal/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
+ self.assert_200(response)
+ self.assert_equal(response.json, MOCK_PORTAL.to_dict())
+
+ def test_get_portal_list(self):
+ """Test case for get_portal_list
+
+ Get a list of portals
+ """
+ query_string = [
+ ("page", 56),
+ ("pageSize", 56),
+ ]
+
+ response = self.client.open(
+ "/api/portal", method="GET", headers=self.headers_no_content, query_string=query_string
+ )
+ self.assert_200(response)
+ expected_response = {
+ "pagination": {
+ "page": 56,
+ "pageSize": 56,
+ "totalItems": 1,
+ "totalPages": 1,
+ },
+ "portals": [MOCK_PORTAL.to_dict()],
+ }
+ assert response.json == expected_response
+
+ def test_update_portal(self):
+ """Test case for update_portal
+
+ Update a portal
+ """
+ updates = {
+ "owner_id": "dd338937-95f0-4b4e-a7a4-0d02da9f56e6",
+ }
+ updated_dict = MOCK_PORTAL.to_dict().copy()
+ updated_dict.update(updates)
+
+ response = self.client.open(
+ f"/api/portal/{MOCK_ID}",
+ method="PUT",
+ headers=self.headers,
+ data=json.dumps(updates),
+ content_type="application/json",
+ )
+ self.assert_200(response)
+ self.assert_equal(response.json, updated_dict)
diff --git a/backend/ibutsu_server/test/test_project_controller.py b/backend/ibutsu_server/test/test_project_controller.py
index 0a18da68..9e387a6b 100644
--- a/backend/ibutsu_server/test/test_project_controller.py
+++ b/backend/ibutsu_server/test/test_project_controller.py
@@ -64,19 +64,14 @@ def test_add_project(self):
self.mock_user.query.get.return_value = MOCK_USER
self.mock_project.query.get.return_value = None
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
response = self.client.open(
"/api/project",
method="POST",
- headers=headers,
+ headers=self.headers,
data=json.dumps(MOCK_DATA),
content_type="application/json",
)
- self.assert_201(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_201(response)
self.assert_equal(response.json, MOCK_PROJECT_DICT)
self.mock_session.add.assert_called_once_with(MOCK_PROJECT)
self.mock_session.commit.assert_called_once()
@@ -89,12 +84,11 @@ def test_get_project_by_id(self):
Get a single project by ID
"""
self.mock_project.query.filter.return_value.first.return_value = None
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open(f"/api/project/{MOCK_ID}", method="GET", headers=headers)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+
+ response = self.client.open(
+ f"/api/project/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
+ self.assert_200(response)
self.assert_equal(response.json, MOCK_PROJECT_DICT)
self.mock_project.query.get.assert_called_once_with(MOCK_ID)
@@ -104,12 +98,11 @@ def test_get_project_by_name(self):
Get a single project by name
"""
self.mock_project.query.filter.return_value.first.return_value = MOCK_PROJECT
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open(f"/api/project/{MOCK_ID}", method="GET", headers=headers)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+
+ response = self.client.open(
+ f"/api/project/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
+ self.assert_200(response)
self.assert_equal(response.json, MOCK_PROJECT_DICT)
def test_get_project_list(self):
@@ -121,14 +114,10 @@ def test_get_project_list(self):
("page", 56),
("pageSize", 56),
]
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
response = self.client.open(
- "/api/project", method="GET", headers=headers, query_string=query_string
+ "/api/project", method="GET", headers=self.headers_no_content, query_string=query_string
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
expected_response = {
"pagination": {
"page": 56,
@@ -151,17 +140,13 @@ def test_update_project(self):
}
updated_dict = MOCK_PROJECT_DICT.copy()
updated_dict.update(updates)
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
f"/api/project/{MOCK_ID}",
method="PUT",
- headers=headers,
+ headers=self.headers,
data=json.dumps(updates),
content_type="application/json",
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
self.assert_equal(response.json, updated_dict)
diff --git a/backend/ibutsu_server/test/test_report_controller.py b/backend/ibutsu_server/test/test_report_controller.py
index 9ab9c45a..24dac018 100644
--- a/backend/ibutsu_server/test/test_report_controller.py
+++ b/backend/ibutsu_server/test/test_report_controller.py
@@ -43,20 +43,16 @@ def test_add_report(self):
Create a new report
"""
body = {"type": "csv", "source": "local"}
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
with patch.dict("ibutsu_server.controllers.report_controller.REPORTS", {"csv": MOCK_CSV}):
response = self.client.open(
"/api/report",
method="POST",
- headers=headers,
+ headers=self.headers,
data=json.dumps(body),
content_type="application/json",
)
- self.assert_201(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_201(response)
assert response.json == MOCK_REPORT_DICT
def test_get_report(self):
@@ -64,12 +60,10 @@ def test_get_report(self):
Get a report
"""
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open(f"/api/report/{MOCK_ID}", method="GET", headers=headers)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ response = self.client.open(
+ f"/api/report/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
+ self.assert_200(response)
assert response.json == MOCK_REPORT_DICT
def test_get_report_list(self):
@@ -82,18 +76,18 @@ def test_get_report_list(self):
mock_limit.return_value.all.return_value = [MOCK_REPORT]
self.mock_report.query.order_by.return_value.offset.return_value.limit = mock_limit
query_string = [("page", 56), ("pageSize", 56)]
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
with patch(
"ibutsu_server.controllers.report_controller.get_project_id"
) as mocked_get_project_id:
mocked_get_project_id.return_value = None
response = self.client.open(
- "/api/report", method="GET", headers=headers, query_string=query_string
+ "/api/report",
+ method="GET",
+ headers=self.headers_no_content,
+ query_string=query_string,
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
expected_response = {
"pagination": {
"page": 56,
diff --git a/backend/ibutsu_server/test/test_result_controller.py b/backend/ibutsu_server/test/test_result_controller.py
index 1b6a89a5..6b1f69ae 100644
--- a/backend/ibutsu_server/test/test_result_controller.py
+++ b/backend/ibutsu_server/test/test_result_controller.py
@@ -101,19 +101,15 @@ def test_add_result(self):
"""
result = ADDED_RESULT.to_dict()
self.mock_result.query.get.return_value = None
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
"/api/result",
method="POST",
- headers=headers,
+ headers=self.headers,
data=json.dumps(result),
content_type="application/json",
)
- self.assert_201(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_201(response)
assert response.json == MOCK_RESULT_DICT
def test_get_result(self):
@@ -121,12 +117,11 @@ def test_get_result(self):
Get a single result
"""
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open(f"/api/result/{MOCK_ID}", method="GET", headers=headers)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+
+ response = self.client.open(
+ f"/api/result/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
+ self.assert_200(response)
assert response.json == MOCK_RESULT_DICT
def test_get_result_list(self):
@@ -144,14 +139,11 @@ def test_get_result_list(self):
("page", 56),
("pageSize", 56),
]
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
- "/api/result", method="GET", headers=headers, query_string=query_string
+ "/api/result", method="GET", headers=self.headers_no_content, query_string=query_string
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
expected_response = {
"pagination": {
"page": 56,
@@ -178,17 +170,13 @@ def test_update_result(self):
"source": "source_updated",
"test_id": "test_id_updated",
}
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
f"/api/result/{MOCK_ID}",
method="PUT",
- headers=headers,
+ headers=self.headers,
data=json.dumps(result),
content_type="application/json",
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
assert response.json == UPDATED_RESULT.to_dict()
diff --git a/backend/ibutsu_server/test/test_run_controller.py b/backend/ibutsu_server/test/test_run_controller.py
index b7193736..3b57d35f 100644
--- a/backend/ibutsu_server/test/test_run_controller.py
+++ b/backend/ibutsu_server/test/test_run_controller.py
@@ -78,20 +78,15 @@ def test_add_run(self):
"start_time": START_TIME,
"created": START_TIME,
}
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
response = self.client.open(
"/api/run",
method="POST",
- headers=headers,
+ headers=self.headers,
data=json.dumps(run_dict),
content_type="application/json",
)
self.project_patcher.stop()
- self.assert_201(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_201(response)
resp = response.json.copy()
resp["project"] = None
self.assert_equal(resp, MOCK_RUN_DICT)
@@ -102,12 +97,11 @@ def test_get_run(self):
Get a single run by ID
"""
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open(f"/api/run/{MOCK_ID}", method="GET", headers=headers)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+
+ response = self.client.open(
+ f"/api/run/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
+ self.assert_200(response)
resp = response.json.copy()
resp["project"] = None
self.assert_equal(resp, MOCK_RUN_DICT)
@@ -118,14 +112,11 @@ def test_get_run_list(self):
Get a list of the test runs
"""
query_string = [("page", 56), ("pageSize", 56)]
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
- "/api/run", method="GET", headers=headers, query_string=query_string
+ "/api/run", method="GET", headers=self.headers_no_content, query_string=query_string
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
@skip("multipart/form-data not supported by Connexion")
def test_import_run(self):
@@ -133,20 +124,16 @@ def test_import_run(self):
Import a JUnit XML file
"""
- headers = {
- "Accept": "application/json",
- "Content-Type": "multipart/form-data",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
data = dict(xml_file=(BytesIO(b"some file data"), "file.txt"))
response = self.client.open(
"/api/run/import",
method="POST",
- headers=headers,
+ headers=self.headers,
data=data,
content_type="multipart/form-data",
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
def test_update_run(self):
"""Test case for update_run
@@ -157,20 +144,16 @@ def test_update_run(self):
"duration": 540.05433,
"summary": {"errors": 1, "failures": 3, "skips": 0, "tests": 548},
}
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
f"/api/run/{MOCK_ID}",
method="PUT",
- headers=headers,
+ headers=self.headers,
data=json.dumps(run_dict),
content_type="application/json",
)
self.mock_update_run_task.apply_async.assert_called_once_with((MOCK_ID,), countdown=5)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
resp = response.json.copy()
resp["project"] = None
self.assert_equal(resp, MOCK_RUN_DICT)
diff --git a/backend/ibutsu_server/test/test_widget_config_controller.py b/backend/ibutsu_server/test/test_widget_config_controller.py
index 225c0ebf..1cbb3e42 100644
--- a/backend/ibutsu_server/test/test_widget_config_controller.py
+++ b/backend/ibutsu_server/test/test_widget_config_controller.py
@@ -2,28 +2,33 @@
from flask import json
-from ibutsu_server.test import BaseTestCase, MockWidgetConfig
-
-# from ibutsu_server.test import MockDashboard
-# from ibutsu_server.test import MockProject
+from ibutsu_server.test import BaseTestCase, MockPortal, MockProject, MockWidgetConfig
MOCK_ID = "91e750be-2ef2-4d85-a50e-2c9366cefd9f"
MOCK_PROJECT_ID = "5ac7d645-45a3-4cbe-acb2-c8d6f7e05468"
+MOCK_PORTAL_ID = "c2ae891a-33c6-11ef-b032-12b95372ee33"
MOCK_DASHBOARD_ID = "5af74747-3b75-4b00-afc3-6304c6f255d7"
-MOCK_WIDGET_CONFIG = MockWidgetConfig(
- id=MOCK_ID,
- navigable=False,
- params={},
- title="Stage builds",
- type="widget",
- weight=0,
- widget="jenkins-heatmap",
- project_id=MOCK_PROJECT_ID,
- dashboard_id=MOCK_DASHBOARD_ID,
+
+
+MOCK_PORTAL = MockPortal.from_dict(
+ id=MOCK_PORTAL_ID,
+ name="unittest-portal",
+ title="UnitTest Portal",
+ owner_id="0eea178e-3306-11ef-b969-12b95372ee33",
+ default_dashboard_id="7e8a1684-3306-11ef-8433-12b95372ee33",
)
-MOCK_WIDGET_CONFIG_DICT = MOCK_WIDGET_CONFIG.to_dict()
-# the result to be POST'ed to Ibutsu, we expect it to transformed into MOCK_RESULT
-ADDED_WIDGET_CONFIG = MockWidgetConfig(
+
+MOCK_PROJECT = MockProject.from_dict(
+ id=MOCK_PROJECT_ID,
+ name="my-project",
+ title="My Project",
+ owner_id="8f22a434-b160-41ed-b700-0cc3d7f146b1",
+ group_id="9af34437-047c-48a5-bd21-6430e4532414",
+ users=[],
+)
+
+# missing project/portal, gets injected at subTest
+MOCK_WIDGET_CONFIG = MockWidgetConfig(
id=MOCK_ID,
navigable=False,
params={},
@@ -31,20 +36,64 @@
type="widget",
weight=0,
widget="jenkins-heatmap",
- project_id=MOCK_PROJECT_ID,
dashboard_id=MOCK_DASHBOARD_ID,
)
-UPDATED_WIDGET_CONFIG = MockWidgetConfig(
- id=MOCK_ID,
- navigable=False,
- params={"jenkins_job_name": "stage"},
- title="Stage builds",
- type="widget",
- weight=10,
- widget="jenkins-heatmap",
- project_id=MOCK_PROJECT_ID,
- dashboard_id=MOCK_DASHBOARD_ID,
+
+# create a complete config with project_id set
+INTERIM = MOCK_WIDGET_CONFIG.to_dict()
+INTERIM.update({"project_id": MOCK_PROJECT_ID})
+MOCK_COMPLETE_WIDGET_CONFIG = MockWidgetConfig.from_dict(**INTERIM)
+
+
+# create an updated config with default weight and a param added
+INTERIM = MOCK_COMPLETE_WIDGET_CONFIG.to_dict()
+INTERIM.update(
+ {
+ "params": {
+ "jenkins_job_name": "stage",
+ }, # value given to PUT call
+ "weight": 10, # default weight applied on update automatically
+ }
)
+MOCK_UPDATED_WIDGET_CONFIG = MockWidgetConfig.from_dict(**INTERIM)
+
+
+VALID_PROJECT_PORTAL_COMBOS = [
+ {"portal_id": MOCK_PORTAL_ID, "portal": None, "project_id": None, "project": None},
+ {"portal_id": None, "portal": None, "project_id": MOCK_PROJECT_ID, "project": None},
+ {"portal_id": None, "portal": MOCK_PORTAL.name, "project_id": None, "project": None},
+ {"portal_id": None, "portal": None, "project_id": None, "project": MOCK_PROJECT.name},
+]
+
+# one of portal, portal_id, project, or project_id should be set
+INVALID_WIDGET_CONFIG_COMBOS = [
+ {
+ "portal_id": MOCK_PORTAL_ID,
+ "portal": None,
+ "project_id": None,
+ "project": {"dummy": "project"}, # the controller should raise before using this
+ },
+ {
+ "portal_id": MOCK_PORTAL_ID,
+ "portal": None,
+ "project_id": MOCK_PROJECT_ID,
+ "project": None,
+ },
+ {
+ "portal_id": None,
+ "portal": {"dummy": "portal"}, # the controller should raise before using this
+ "project_id": MOCK_PROJECT_ID,
+ "project": None,
+ },
+ {
+ "portal_id": None,
+ "portal": {"dummy": "portal"}, # the controller should raise before using this,
+ "project_id": None,
+ "project": {"dummy": "project"}, # the controller should raise before using this
+ },
+ {"portal_id": None, "portal": None, "project_id": None, "project": None},
+ {"portal_id": MOCK_PORTAL_ID, "widget": "junk-widget-type"},
+]
class TestWidgetConfigController(BaseTestCase):
@@ -52,20 +101,30 @@ class TestWidgetConfigController(BaseTestCase):
def setUp(self):
"""Set up tests"""
+ # mock the db session
self.session_patcher = patch("ibutsu_server.controllers.widget_config_controller.session")
self.mock_session = self.session_patcher.start()
+ # mock the project_has_user return
self.project_has_user_patcher = patch(
"ibutsu_server.controllers.widget_config_controller.project_has_user"
)
self.mock_project_has_user = self.project_has_user_patcher.start()
self.mock_project_has_user.return_value = True
+ # mock the WidgetConfig class
self.widget_config_patcher = patch(
"ibutsu_server.controllers.widget_config_controller.WidgetConfig"
)
self.mock_widget_config = self.widget_config_patcher.start()
- self.mock_widget_config.return_value = MOCK_WIDGET_CONFIG
- self.mock_widget_config.query.get.return_value = MOCK_WIDGET_CONFIG
- self.mock_widget_config.from_dict.return_value = ADDED_WIDGET_CONFIG
+ # mock the get_portal response
+ self.get_portal_patcher = patch(
+ "ibutsu_server.controllers.widget_config_controller.get_portal"
+ )
+ self.mock_get_portal = self.get_portal_patcher.start()
+ # mock the get_project response
+ self.get_project_patcher = patch(
+ "ibutsu_server.controllers.widget_config_controller.get_project"
+ )
+ self.mock_get_project = self.get_project_patcher.start()
def tearDown(self):
"""Teardown the mocks"""
@@ -73,53 +132,115 @@ def tearDown(self):
self.project_has_user_patcher.stop()
self.session_patcher.stop()
+ def mock_widget_config_returns(
+ self, config_dict=None, from_dict_obj=None, portal=None, project=None
+ ):
+ self.mock_widget_config.return_value = config_dict or MOCK_COMPLETE_WIDGET_CONFIG.to_dict()
+ self.mock_widget_config.query.get.return_value = (
+ from_dict_obj or MOCK_COMPLETE_WIDGET_CONFIG
+ )
+ self.mock_widget_config.from_dict.return_value = (
+ from_dict_obj or MOCK_COMPLETE_WIDGET_CONFIG
+ )
+ self.mock_get_portal.return_value = portal
+ self.mock_get_project.return_value = project
+
def test_add_widget_config(self):
"""Test case for add_widget_config
Create a test widget_config
"""
- widget_config = ADDED_WIDGET_CONFIG.to_dict()
- self.mock_widget_config.query.get.return_value = None
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open(
- "/api/widget-config",
- method="POST",
- headers=headers,
- data=json.dumps(widget_config),
- content_type="application/json",
- )
- self.assert_201(response, "Response body is : " + response.data.decode("utf-8"))
- assert response.json == MOCK_WIDGET_CONFIG_DICT
+
+ for config in VALID_PROJECT_PORTAL_COMBOS:
+ widget_config_dict = MOCK_COMPLETE_WIDGET_CONFIG.to_dict()
+ with self.subTest(config=config):
+ # overwrite project/portal/project_id/portal_id with subtest config
+ widget_config_dict.update(config)
+ # run the updated dict through from_dict.to_dict and mock the return to None
+ MOCK_WC_WITH_PROJECT_PORTAL = MockWidgetConfig.from_dict(**widget_config_dict)
+ # Figure out whether to patch the get_portal or get_project function
+ PORTAL_OR_PROJ = {}
+ if config.get("portal") is not None:
+ PORTAL_OR_PROJ = {"portal": MOCK_PORTAL}
+ if config.get("project") is not None:
+ PORTAL_OR_PROJ = {"project": MOCK_PROJECT}
+ # Inject the mock return values based on subTest config
+ self.mock_widget_config_returns(
+ MOCK_WC_WITH_PROJECT_PORTAL.to_dict(),
+ MOCK_WC_WITH_PROJECT_PORTAL,
+ **PORTAL_OR_PROJ,
+ )
+ self.mock_widget_config.query.get.return_value = None
+
+ response = self.client.open(
+ "/api/widget-config",
+ method="POST",
+ headers=self.headers,
+ data=json.dumps(widget_config_dict),
+ content_type="application/json",
+ )
+ self.assert_201(response)
+ assert response.json == MOCK_WC_WITH_PROJECT_PORTAL.to_dict()
+ widget_config_dict = MOCK_COMPLETE_WIDGET_CONFIG.to_dict()
+
+ # TODO: test 403 on add
+
+ def test_add_widget_invalid_config_bad_request(self):
+ """Test case for adding invalid widget config to produce BAD REQUEST
+
+ Should not create a WC
+ """
+
+ for config in INVALID_WIDGET_CONFIG_COMBOS:
+ # use the incomplete mock to inject project/portal/ids
+ widget_config_dict = MOCK_WIDGET_CONFIG.to_dict()
+
+ with self.subTest(config=config):
+ widget_config_dict.update(config)
+ MOCK_WIDGET_CONFIG_FULL = MockWidgetConfig.from_dict(**widget_config_dict)
+ self.mock_widget_config_returns(
+ MOCK_WIDGET_CONFIG_FULL.to_dict(),
+ MOCK_WIDGET_CONFIG_FULL,
+ )
+ self.mock_widget_config.query.get.return_value = None
+
+ response = self.client.open(
+ "/api/widget-config",
+ method="POST",
+ headers=self.headers,
+ data=json.dumps(widget_config_dict),
+ content_type="application/json",
+ )
+
+ self.assert_400(response)
def test_get_widget_config(self):
"""Test case for get_widget_config
Get a single widget_config
"""
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open(f"/api/widget-config/{MOCK_ID}", method="GET", headers=headers)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
- assert response.json == MOCK_WIDGET_CONFIG_DICT
+ # mock in the full config for an existing widget_config
+ self.mock_widget_config_returns()
+
+ response = self.client.open(
+ f"/api/widget-config/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
+ self.assert_200(response)
+ assert response.json == MOCK_COMPLETE_WIDGET_CONFIG.to_dict()
def test_get_widget_config_404(self):
"""Test case for get_widget_config
Return a 404 when no widget config is found
"""
- self.mock_widget_config.query.get.return_value = None
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open(f"/api/widget-config/{MOCK_ID}", method="GET", headers=headers)
- self.assert_404(response, "Response body is : " + response.data.decode("utf-8"))
+ # mock in the full config for an existing widget_config
+ self.mock_widget_config_returns()
+ self.mock_widget_config.query.get.return_value = None # override query to trigger 404
+
+ response = self.client.open(
+ f"/api/widget-config/{MOCK_ID}", method="GET", headers=self.headers_no_content
+ )
+ self.assert_404(response)
def test_get_widget_config_list(self):
"""Test case for get_widget_config_list
@@ -130,15 +251,14 @@ def test_get_widget_config_list(self):
mock_query = self.mock_widget_config.query
mock_query.order_by.return_value.offset.return_value.limit.return_value.all = mock_all
mock_query.count.return_value = 1
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
- response = self.client.open("/api/widget-config", method="GET", headers=headers)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+
+ response = self.client.open(
+ "/api/widget-config", method="GET", headers=self.headers_no_content
+ )
+ self.assert_200(response)
expected_response = {
"pagination": {"page": 1, "pageSize": 25, "totalItems": 1, "totalPages": 1},
- "widgets": [MOCK_WIDGET_CONFIG_DICT],
+ "widgets": [MOCK_WIDGET_CONFIG.to_dict()],
}
assert response.json == expected_response
@@ -146,52 +266,80 @@ def test_update_widget_config(self):
"""Test case for update_widget_config
Updates a single widget_config
+ TODO: expand for testing project/portal fetch. same pattern as the add_widget_config controller
+ TODO: cover navigable + type logic in the update controller
"""
- widget_config = {
+ widget_config_update = {
"params": {
"jenkins_job_name": "stage",
},
}
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+ # mock in the full config for an existing widget_config
+ self.mock_widget_config_returns()
+
+ # try to update params, weight should also automatically move from 0 to 10
response = self.client.open(
f"/api/widget-config/{MOCK_ID}",
method="PUT",
- headers=headers,
- data=json.dumps(widget_config),
+ headers=self.headers,
+ data=json.dumps(widget_config_update),
content_type="application/json",
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
- assert response.json == UPDATED_WIDGET_CONFIG.to_dict()
+ self.assert_200(response)
+ assert response.json == MOCK_UPDATED_WIDGET_CONFIG.to_dict()
+
+ # TODO: test 403 on update
+
+ def test_update_widget_invalid_config_bad_request(self):
+ """Test case for updating invalid widget config to produce BAD REQUEST
+
+ Should not update the target WC
+ """
+
+ # mock in the full config for an existing widget_config
+ self.mock_widget_config_returns()
+
+ for config in INVALID_WIDGET_CONFIG_COMBOS:
+ with self.subTest(config=config):
+ # reset widget config on each subtest
+ widget_config_dict = MOCK_WIDGET_CONFIG.to_dict()
+ widget_config_dict.update(config)
+ # reset mocks on subtest
+ self.mock_widget_config_returns()
+ self.mock_widget_config.query.get.return_value = None
+
+ response = self.client.open(
+ "/api/widget-config/{MOCK_ID}",
+ method="PUT",
+ headers=self.headers,
+ data=json.dumps(widget_config_dict),
+ content_type="application/json",
+ )
+
+ self.assert_400(response)
def test_delete_widget_config(self):
"""Test the deletion of widget configs"""
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
+ # mock in the full config for an existing widget_config
+ self.mock_widget_config_returns()
+
response = self.client.open(
f"/api/widget-config/{MOCK_ID}",
method="DELETE",
- headers=headers,
+ headers=self.headers,
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
+
+ # TODO: test 403 on delete
def test_delete_widget_config_404(self):
"""Test that trying to delete a non-existant widget_config throws a 404"""
self.mock_widget_config.query.get.return_value = None
- headers = {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
"/api/widget-config/{id}".format(id="885021da-4a73-4110-9024-eff12b66ce19"),
method="DELETE",
- headers=headers,
+ headers=self.headers,
)
- self.assert_404(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_404(response)
diff --git a/backend/ibutsu_server/test/test_widget_controller.py b/backend/ibutsu_server/test/test_widget_controller.py
index cb557a64..6dff6d95 100644
--- a/backend/ibutsu_server/test/test_widget_controller.py
+++ b/backend/ibutsu_server/test/test_widget_controller.py
@@ -70,17 +70,14 @@ def test_get_comparison_result_list(self, mocked_query):
query_string = {
"filters": ["metadata.component=frontend", "metadata.component=frontend"],
}
- headers = {
- "Accept": "application/json",
- "Authorization": f"Bearer {self.jwt_token}",
- }
+
response = self.client.open(
"/api/widget/compare-runs-view",
method="GET",
- headers=headers,
+ headers=self.headers_no_content,
query_string=query_string,
)
- self.assert_200(response, "Response body is : " + response.data.decode("utf-8"))
+ self.assert_200(response)
expected_response = {
"pagination": {"totalItems": 1},
"results": [MOCK_RESULTS_DICT],
diff --git a/backend/ibutsu_server/util/portals.py b/backend/ibutsu_server/util/portals.py
new file mode 100644
index 00000000..89c24f0b
--- /dev/null
+++ b/backend/ibutsu_server/util/portals.py
@@ -0,0 +1,17 @@
+from ibutsu_server.db.models import Portal
+from ibutsu_server.util.uuid import is_uuid
+
+
+def get_portal(portal_name):
+ """Perform a lookup to return the actual portal record"""
+ if is_uuid(portal_name):
+ portal = Portal.query.get(portal_name)
+ else:
+ portal = Portal.query.filter(Portal.name == portal_name).first()
+ return portal
+
+
+def get_portal_id(portal_name):
+ """Shorthand function for a repeated piece of code"""
+ portal = get_portal(portal_name)
+ return str(portal.id) if portal else None
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 00000000..74099e72
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,8 @@
+// eslint.config.js
+export default [
+ {
+ rules: {
+ "no-unused-vars": "warn", // this isn't actually working through pre-commit or webpack
+ }
+ }
+];
diff --git a/frontend/package.json b/frontend/package.json
index e055af62..138b8c73 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,9 +6,11 @@
"@babel/core": "^7.24.7",
"@babel/eslint-parser": "^7.24.7",
"@babel/helper-call-delegate": "^7.12.13",
+ "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-syntax-jsx": "^7.24.7",
"@babel/plugin-transform-class-properties": "^7.24.7",
"@babel/plugin-transform-private-methods": "^7.24.7",
+ "@babel/plugin-transform-private-property-in-object": "^7.24.7",
"@babel/preset-flow": "^7.24.7",
"@babel/preset-react": "^7.24.7",
"@greatsumini/react-facebook-login": "^3.3.3",
@@ -44,6 +46,9 @@
"typescript": "^4.9.5",
"wolfy87-eventemitter": "^5.2.9"
},
+ "devDependencies": {
+ "@babel/plugin-proposal-private-property-in-object": "^7.21.11"
+ },
"scripts": {
"start": "serve -s build -l tcp://0.0.0.0:8080",
"build": "./bin/write-version-file.js && react-scripts build",
diff --git a/frontend/src/admin.js b/frontend/src/admin.js
index fadabc17..291ea299 100644
--- a/frontend/src/admin.js
+++ b/frontend/src/admin.js
@@ -1,22 +1,20 @@
import React from 'react';
-import {
- Nav,
- NavList
-} from '@patternfly/react-core';
-
-import { NavLink, Route, Routes } from 'react-router-dom';
+import { Navigate, Route, Routes } from 'react-router-dom';
import EventEmitter from 'wolfy87-eventemitter';
import ElementWrapper from './components/elementWrapper';
-import { IbutsuPage } from './components';
-import { AdminHome } from './pages/admin/home';
+import AdminHome from './pages/admin/home';
import { UserList } from './pages/admin/user-list';
import { UserEdit } from './pages/admin/user-edit';
import { ProjectList } from './pages/admin/project-list';
import { ProjectEdit } from './pages/admin/project-edit';
import { AuthService } from './services/auth';
+import { PortalList } from './pages/admin/portal-list';
+import { PortalEdit } from './pages/admin/portal-edit';
+
import './app.css';
+import AdminPage from './components/admin-page';
export class Admin extends React.Component {
@@ -34,34 +32,22 @@ export class Admin extends React.Component {
}
render() {
- const navigation = (
-
- );
-
return (
-
-
-
- }/>
- } />
- } />
- } />
- } />
-
-
-
+
+ }
+ >
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ }/>
+
);
}
}
diff --git a/frontend/src/app.js b/frontend/src/app.js
index 13bf29ef..8ebfb995 100644
--- a/frontend/src/app.js
+++ b/frontend/src/app.js
@@ -1,13 +1,9 @@
import React from 'react';
-import {
- Nav,
- NavList
-} from '@patternfly/react-core';
import EventEmitter from 'wolfy87-eventemitter';
import ElementWrapper from './components/elementWrapper';
-import { NavLink, Route, Routes } from 'react-router-dom';
+import { Route, Routes } from 'react-router-dom';
import { Dashboard } from './dashboard';
import { ReportBuilder } from './report-builder';
@@ -15,14 +11,14 @@ import { RunList } from './run-list';
import { Run } from './run';
import { ResultList } from './result-list';
import { Result } from './result';
-import { Settings } from './settings';
import { View, IbutsuPage } from './components';
-import { HttpClient } from './services/http';
-import { getActiveProject } from './utilities';
+import { IbutsuContext } from './services/context';
+
import './app.css';
export class App extends React.Component {
+ static contextType = IbutsuContext;
constructor(props) {
super(props);
this.eventEmitter = new EventEmitter();
@@ -33,79 +29,86 @@ export class App extends React.Component {
searchValue: '',
views: []
};
- this.eventEmitter.on('projectChange', () => {
- this.getViews();
- });
- }
-
- getViews() {
- let params = {'filter': ['type=view', 'navigable=true']};
- let project = getActiveProject();
- if (project) {
- params['filter'].push('project_id=' + project.id);
- }
- HttpClient.get([Settings.serverUrl, 'widget-config'], params)
- .then(response => HttpClient.handleResponse(response))
- .then(data => {
- data.widgets.forEach(widget => {
- if (project) {
- widget.params['project'] = project.id;
- }
- else {
- delete widget.params['project'];
- }
- });
- this.setState({views: data.widgets});
- });
}
componentDidMount() {
- this.getViews();
}
render() {
document.title = 'Ibutsu';
- const { views } = this.state;
- const navigation = (
-
- );
-
return (
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
+
+ }
+ />
+ }
+ >
+
+ {/* Nested project routes */}
+
+ }
+ />
+ }
+ />
+
+
+
+ }
+ />
+ }
+ />
+
+ }
+ />
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+ {/* } /> */}
+
+
+ {/* Nested Portal routes */}
+ {/* }
+ />
+ }
+ />
+ }
+ /> */}
+
+
+
);
}
}
diff --git a/frontend/src/base.js b/frontend/src/base.js
index 0c27cd18..c66a7971 100644
--- a/frontend/src/base.js
+++ b/frontend/src/base.js
@@ -3,35 +3,40 @@ import React from 'react';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import { App } from './app';
import { Admin } from './admin';
-import { Profile } from './profile';
+import Profile from './profile';
import { Login } from './login';
import { SignUp } from './sign-up';
import { ForgotPassword } from './forgot-password';
import { ResetPassword } from './reset-password';
import { AuthService } from './services/auth';
import ElementWrapper from './components/elementWrapper';
+import { IbutsuContextProvider } from './services/context';
export const Base = () => {
return (
-
-
- } />
- } />
- } />
- } />
- : }
- />
- : }
- />
- : }
- />
-
-
+
+
+
+ } />
+ } />
+ } />
+ } />
+ : }
+ />
+ : }
+ />
+ : }
+ />
+ } />
+
+
+
);
};
diff --git a/frontend/src/components/admin-page.js b/frontend/src/components/admin-page.js
new file mode 100644
index 00000000..81ca2812
--- /dev/null
+++ b/frontend/src/components/admin-page.js
@@ -0,0 +1,62 @@
+import React from 'react';
+
+import {
+ Nav,
+ NavList,
+ Page
+} from '@patternfly/react-core';
+
+import { Link, Outlet } from 'react-router-dom';
+import ElementWrapper from './elementWrapper';
+
+import { IbutsuHeader } from './ibutsu-header';
+import PropTypes from 'prop-types';
+
+
+
+const AdminPage = (props) => {
+ // TODO useEffect instead of eventEmitter prop
+ // TODO notifications on admin page with state and AlertGroup
+ PropTypes
+ const navigation = (
+ // TODO what is onNavSelect doing here ...
+
+ );
+
+ document.title = 'Administration | Ibutsu';
+
+ return (
+
+ }
+ sidebar={navigation}
+ isManagedSidebar={true}
+ style={{position: "relative"}}
+ >
+
+
+
+ );
+};
+
+AdminPage.propTypes = {
+ eventEmitter: PropTypes.object,
+};
+
+export default AdminPage;
diff --git a/frontend/src/components/elementWrapper.js b/frontend/src/components/elementWrapper.js
index 0d43e226..32c040ff 100644
--- a/frontend/src/components/elementWrapper.js
+++ b/frontend/src/components/elementWrapper.js
@@ -11,7 +11,7 @@ const ElementWrapper = (props) => {
const Element = props.routeElement;
const eventEmitter = props.eventEmitter
- return ;
+ return ;
};
ElementWrapper.propTypes = {
diff --git a/frontend/src/components/filtertable.js b/frontend/src/components/filtertable.js
index a411a5b4..fad16a4d 100644
--- a/frontend/src/components/filtertable.js
+++ b/frontend/src/components/filtertable.js
@@ -24,9 +24,10 @@ import {
import { Settings } from '../settings';
import { HttpClient } from '../services/http';
-import { getActiveProject, toAPIFilter } from '../utilities';
+import { toAPIFilter } from '../utilities';
import { TableEmptyState, TableErrorState } from './tablestates';
+import { IbutsuContext } from '../services/context';
export class FilterTable extends React.Component {
static propTypes = {
@@ -173,6 +174,7 @@ export class FilterTable extends React.Component {
// TODO Extend this to contain the filter handling functions, and better integrate filter state
// with FilterTable. See https://github.com/ibutsu/ibutsu-server/issues/230
export class MetaFilter extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
runId: PropTypes.string,
setFilter: PropTypes.func,
@@ -257,8 +259,9 @@ export class MetaFilter extends React.Component {
let api_filter = toAPIFilter(customFilters).join();
console.debug('APIFILTER: ' + customFilters);
- let project = getActiveProject();
- let projectId = project ? project.id : ''
+ // TODO handle portal
+ const { primaryObject } = this.context;
+ let projectId = primaryObject ? primaryObject.id : ''
// make runId optional
let params = {}
@@ -290,8 +293,8 @@ export class MetaFilter extends React.Component {
}
getProjectFilterParams() {
- let project = getActiveProject();
- HttpClient.get([Settings.serverUrl, 'project', 'filter-params', project.id])
+ const { primaryObject } = this.context;
+ HttpClient.get([Settings.serverUrl, 'project', 'filter-params', primaryObject.id])
.then(response => HttpClient.handleResponse(response))
.then(data => {
this.setState({fieldOptions: data});
diff --git a/frontend/src/components/ibutsu-header.js b/frontend/src/components/ibutsu-header.js
index 28aa6e80..56a164d4 100644
--- a/frontend/src/components/ibutsu-header.js
+++ b/frontend/src/components/ibutsu-header.js
@@ -34,40 +34,94 @@ import {
import { BarsIcon, MoonIcon, ServerIcon, TimesIcon, QuestionCircleIcon, UploadIcon } from '@patternfly/react-icons';
import { FileUpload, UserDropdown } from '../components';
-import { MONITOR_UPLOAD_TIMEOUT } from '../constants';
+import { MONITOR_UPLOAD_TIMEOUT, VERSION_CHECK_TIMEOUT } from '../constants';
+import packageJson from '../../package.json'
import { HttpClient } from '../services/http';
import { Settings } from '../settings';
-import { getActiveProject, getTheme, setTheme } from '../utilities';
+import { getDateString, getTheme, setTheme } from '../utilities';
+import { IbutsuContext } from '../services/context';
export class IbutsuHeader extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
eventEmitter: PropTypes.object,
navigate: PropTypes.func,
- version: PropTypes.string
+ version: PropTypes.string,
+ params: PropTypes.object,
}
constructor(props) {
super(props);
- let project = getActiveProject();
this.eventEmitter = props.eventEmitter;
+ this.versionCheckId = '';
this.state = {
+ // version
+ version: packageJson.version,
+ // upload state
uploadFileName: '',
importId: '',
monitorUploadId: null,
- isAboutOpen: false,
+ // project state
isProjectSelectorOpen: false,
- selectedProject: project || '',
- inputValue: project?.title || '',
+ selectedProject: '',
+ inputValue: '',
filterValue: '',
projects: [],
filteredProjects: [],
+ // portal state
+ isPortalSelectorOpen: false,
+ selectedPortal: '',
+ portalInputValue: '',
+ portalFilterValue: '',
+ portals: [],
+ filteredPortals: [],
+ // misc
+ isAboutOpen: false,
isDarkTheme: getTheme() === 'dark',
- version: props.version
};
}
+ sync_context = () => {
+ // TODO handle portal_id
+ // Primary object
+ const { primaryObject, setPrimaryObject, setPrimaryType } = this.context;
+ const { selectedProject } = this.state;
+ const paramProject = this.props.params?.project_id;
+ let updatedPrimary = undefined;
+
+ // API fetch and set the context
+ if (paramProject && primaryObject?.id !== paramProject) {
+ HttpClient.get([Settings.serverUrl, 'project', paramProject])
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ updatedPrimary = data;
+ setPrimaryObject(data)
+ setPrimaryType('project')
+ // update state
+ this.setState({
+ selectedProject: data,
+ isProjectSelectorOpen: false,
+ inputValue: data?.title,
+ filterValue: ''
+ });
+ });
+ }
+
+ // update selector state
+ if (updatedPrimary && !selectedProject) {
+ this.setState({
+ selectedProject: updatedPrimary,
+ inputValue: updatedPrimary.title
+ })
+ }
+
+ if ( updatedPrimary ) {
+ this.emitProjectChange(updatedPrimary);
+ }
+ }
+
showNotification(type, title, message, action = null, timeout = null, key = null) {
if (!this.eventEmitter) {
return;
@@ -75,11 +129,37 @@ export class IbutsuHeader extends React.Component {
this.eventEmitter.emit('showNotification', type, title, message, action, timeout, key);
}
- emitProjectChange() {
+ checkVersion() {
+ const frontendUrl = window.location.origin;
+ HttpClient.get([frontendUrl, 'version.json'], {'v': getDateString()})
+ .then(response => HttpClient.handleResponse(response))
+ .then((data) => {
+ if (data && data.version && (data.version !== this.state.version)) {
+ const action = { window.location.reload(); }}>Reload;
+ this.showNotification(
+ 'info',
+ 'Ibutsu has been updated',
+ 'A newer version of Ibutsu is available, click reload to get it.',
+ action,
+ true,
+ 'check-version');
+ }
+ });
+ }
+
+ emitProjectChange(value = null) {
if (!this.eventEmitter) {
return;
}
- this.eventEmitter.emit('projectChange');
+ this.eventEmitter.emit('projectChange', value);
+ }
+
+ emitPortalChange() {
+ // the portal selector doesnt even exist yet
+ if (!this.eventEmitter) {
+ return;
+ }
+ this.eventEmitter.emit('portalChange');
}
emitThemeChange() {
@@ -89,14 +169,24 @@ export class IbutsuHeader extends React.Component {
this.eventEmitter.emit('themeChange');
}
- getProjects() {
- const params = {pageSize: 10};
+ getSelectorOptions = (endpoint = "project") => {
+ // adding s here seems dumb, but this scope is small, it's only abstracted for 2 things
+ // TODO: iterate over pages, fix controller filtering behavior to apply pageSize _after_ filter
+ const pluralEndpoint = endpoint+'s';
+ const params = {pageSize: 20};
if (this.state.filterValue) {
params['filter'] = ['title%' + this.state.filterValue];
}
- HttpClient.get([Settings.serverUrl, 'project'], params)
+ HttpClient.get([Settings.serverUrl, endpoint], params)
.then(response => HttpClient.handleResponse(response))
- .then(data => this.setState({projects: data['projects'], filteredProjects: data['projects']}));
+ .then(data => {
+ this.setState(
+ {
+ projects: data[pluralEndpoint],
+ filteredProjects: data[pluralEndpoint],
+ })
+ }
+ );
}
onBeforeUpload = (files) => {
@@ -144,11 +234,11 @@ export class IbutsuHeader extends React.Component {
onProjectToggle = () => {
this.setState({isProjectSelectorOpen: !this.state.isProjectSelectorOpen});
- };
+ }
onProjectSelect = (_event, value) => {
- const activeProject = getActiveProject();
- if (activeProject && activeProject.id === value.id) {
+ const { primaryObject, setPrimaryObject, setPrimaryType } = this.context;
+ if (primaryObject?.id === value?.id) {
this.setState({
isProjectSelectorOpen: false,
inputValue: value.title,
@@ -156,39 +246,49 @@ export class IbutsuHeader extends React.Component {
});
return;
}
-
- const project = JSON.stringify(value);
- localStorage.setItem('project', project);
+ // update context
+ setPrimaryObject(value)
+ setPrimaryType('project')
+ // update state
this.setState({
selectedProject: value,
isProjectSelectorOpen: false,
- inputValue: value.title,
+ inputValue: value?.title,
filterValue: ''
});
- this.emitProjectChange();
- };
+ // Consider whether the location should be changed within the emit hooks?
+ this.props.navigate('/project/' + value?.id + '/dashboard/' + value?.default_dashboard_id);
+
+ // useEffect with dependency on functional component to remove passing value, handlers don't see updated context
+ this.emitProjectChange(value);
+ }
onProjectClear = () => {
- localStorage.removeItem('project');
+ const { setPrimaryObject } = this.context;
+
this.setState({
selectedProject: '',
isProjectSelectorOpen: false,
inputValue: '',
filterValue: ''
- }, this.getProjects);
+ });
+ setPrimaryObject();
+
+ this.props.navigate("/project");
+
this.emitProjectChange();
}
- onTextInputChange = (_event, value) => {
+ onProjectTextInputChange = (_event, value) => {
this.setState({
inputValue: value,
filterValue: value
- }, this.getProjects);
- };
+ }, this.getSelectorOptions('project'));
+ }
toggleAbout = () => {
this.setState({isAboutOpen: !this.state.isAboutOpen});
- };
+ }
onThemeChanged = (isChecked) => {
setTheme(isChecked ? 'dark' : 'light');
@@ -199,10 +299,16 @@ export class IbutsuHeader extends React.Component {
if (this.state.monitorUploadId) {
clearInterval(this.state.monitorUploadId);
}
+ if (this.versionCheckId) {
+ clearInterval(this.versionCheckId);
+ }
}
componentDidMount() {
- this.getProjects();
+ this.getSelectorOptions("project");
+ this.sync_context();
+ this.checkVersion();
+ this.versionCheckId = setInterval(() => this.checkVersion(), VERSION_CHECK_TIMEOUT);
}
componentDidUpdate(prevProps, prevState) {
@@ -238,7 +344,7 @@ export class IbutsuHeader extends React.Component {
{
this.showNotification(type, title, message, action, timeout, key);
});
this.props.eventEmitter.on('themeChange', this.setTheme);
+ this.props.eventEmitter.on('projectChange', () => {
+ });
+ // TODO: empty state props.children override
+
}
showNotification(type, title, message, action, timeout, key) {
@@ -81,32 +88,13 @@ export class IbutsuPage extends React.Component {
}
}
- checkVersion() {
- const frontendUrl = window.location.origin;
- HttpClient.get([frontendUrl, 'version.json'], {'v': getDateString()})
- .then(response => HttpClient.handleResponse(response))
- .then((data) => {
- if (data && data.version && (data.version !== this.state.version)) {
- const action = { window.location.reload(); }}>Reload;
- this.showNotification('info', 'Ibutsu has been updated', 'A newer version of Ibutsu is available, click reload to get it.', action, true, 'check-version');
- }
- });
- }
-
- componentWillUnmount() {
- if (this.versionCheckId) {
- clearInterval(this.versionCheckId);
- }
- }
-
componentDidMount() {
this.setTheme();
- this.checkVersion();
- this.versionCheckId = setInterval(() => this.checkVersion(), VERSION_CHECK_TIMEOUT);
}
render() {
document.title = this.props.title || 'Ibutsu';
+ // TODO: render project or portal depending on menutoggle + select event
return (
@@ -117,17 +105,12 @@ export class IbutsuPage extends React.Component {
))}
}
- sidebar={
-
-
- {this.props.navigation}
-
- }
+ header={}
+ sidebar={}
isManagedSidebar={true}
style={{position: "relative"}}
>
- {this.props.children}
+
);
diff --git a/frontend/src/components/portals-page.js b/frontend/src/components/portals-page.js
new file mode 100644
index 00000000..85f7872a
--- /dev/null
+++ b/frontend/src/components/portals-page.js
@@ -0,0 +1 @@
+{/* TODO: portals-page, will mirror the projects::dashboard view on ibutsu-page currently */}
diff --git a/frontend/src/components/profile-page.js b/frontend/src/components/profile-page.js
new file mode 100644
index 00000000..56e7d1fa
--- /dev/null
+++ b/frontend/src/components/profile-page.js
@@ -0,0 +1,55 @@
+import React from 'react';
+
+import {
+ Nav,
+ NavList,
+ Page
+} from '@patternfly/react-core';
+
+import { NavLink, Outlet} from 'react-router-dom';
+
+
+import ElementWrapper from './elementWrapper';
+import { IbutsuHeader } from './ibutsu-header';
+import PropTypes from 'prop-types';
+
+
+
+const ProfilePage = (props) => {
+ // TODO useEffect
+
+ const navigation = (
+ // TODO what is onNavSelect doing here ...
+
+ );
+
+ document.title = "Profile | Ibutsu";
+
+ return (
+
+ }
+ sidebar={navigation}
+ isManagedSidebar={true}
+ style={{position: "relative"}}
+ >
+
+
+
+ );
+}
+
+ProfilePage.propTypes = {
+ eventEmitter: PropTypes.object,
+};
+
+export default ProfilePage
diff --git a/frontend/src/components/result.js b/frontend/src/components/result.js
index 7261297e..fb8ead95 100644
--- a/frontend/src/components/result.js
+++ b/frontend/src/components/result.js
@@ -239,7 +239,7 @@ export class ResultView extends React.Component {
resultIcon = getIconForResult(testResult.result);
startTime = new Date(testResult.start_time);
parameters = Object.keys(testResult.params).map((key) => {key} = {testResult.params[key]}
);
- runLink = {testResult.run_id};
+ runLink = {testResult.run_id};
}
const jsonViewTheme = {
scheme: 'monokai',
@@ -296,7 +296,7 @@ export class ResultView extends React.Component {
Component:,
- {testResult.component}
+ {testResult.component}
]}
/>
@@ -499,7 +499,7 @@ export class ResultView extends React.Component {
Source:,
- {testResult.source}
+ {testResult.source}
]}
/>
diff --git a/frontend/src/components/sidebar.js b/frontend/src/components/sidebar.js
new file mode 100644
index 00000000..31937b89
--- /dev/null
+++ b/frontend/src/components/sidebar.js
@@ -0,0 +1,91 @@
+
+import React, { useContext, useState } from "react";
+import PropTypes from 'prop-types';
+
+import { Link } from 'react-router-dom';
+import { IbutsuContext } from "../services/context";
+import { PageSidebar,
+ PageSidebarBody,
+ Nav,
+ NavList,
+ } from '@patternfly/react-core';
+import { HttpClient } from "../services/http";
+import { Settings } from "../settings";
+
+
+const IbutsuSidebar = (props) => {
+ const context = useContext(IbutsuContext);
+ const [views, setViews] = useState();
+ props.eventEmitter.on('projectChange', (project) => {
+ setProjectViews(project); // TODO somehow this is getting triggered multiple times on project select
+ })
+ // const params = useParams();
+
+
+ function setProjectViews(project) {
+ const { primaryObject } = context;
+ const targetProject = project ?? primaryObject;
+
+ let params = {'filter': ['type=view', 'navigable=true']};
+
+ // read selected project from location
+ params['filter'].push('project_id=' + targetProject.id);
+
+ HttpClient.get([Settings.serverUrl, 'widget-config'], params)
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ //debugger; //eslint-disable-line no-debugger
+
+ data.widgets.forEach(widget => {
+ if (targetProject) {
+ widget.params['project'] = targetProject.id;
+ }
+ else {
+ delete widget.params['project'];
+ }
+ });
+ // TODO: views just part of the context instead of state of this component?
+ console.log('setting views: ');
+ console.log(data.widgets);
+ setViews(data.widgets)});
+ }
+
+ const { primaryType, primaryObject } = context;
+ if ( primaryType == 'project' && primaryObject ) {
+ return (
+
+
+
+
+
+ );
+ }
+};
+
+IbutsuSidebar.propTypes = {
+ eventEmitter: PropTypes.object,
+};
+
+export default IbutsuSidebar;
diff --git a/frontend/src/components/test-history.js b/frontend/src/components/test-history.js
index 6379a16b..f4419fc2 100644
--- a/frontend/src/components/test-history.js
+++ b/frontend/src/components/test-history.js
@@ -214,7 +214,7 @@ export class TestHistoryTable extends React.Component {
.then(data => this.setState({
lastPassedDate:
-
+
{new Date(data.results[0].start_time).toLocaleString()}
diff --git a/frontend/src/components/user-dropdown.js b/frontend/src/components/user-dropdown.js
index e2fe3b29..b405018b 100644
--- a/frontend/src/components/user-dropdown.js
+++ b/frontend/src/components/user-dropdown.js
@@ -80,11 +80,11 @@ export class UserDropdown extends React.Component {
>
- Profile
+ Profile
{!!this.state.isSuperAdmin &&
- Administration
+ Administration
}
diff --git a/frontend/src/dashboard.js b/frontend/src/dashboard.js
index 486eb49e..28581476 100644
--- a/frontend/src/dashboard.js
+++ b/frontend/src/dashboard.js
@@ -46,67 +46,85 @@ import {
ResultAggregatorWidget,
ResultSummaryWidget
} from './widgets';
-import { getActiveProject, getActiveDashboard } from './utilities.js';
+import { IbutsuContext } from './services/context.js';
export class Dashboard extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
- eventEmitter: PropTypes.object
+ eventEmitter: PropTypes.object,
+ navigate: PropTypes.func,
+ params: PropTypes.object,
}
constructor(props) {
super(props);
- let dashboard = getActiveDashboard() || this.getDefaultDashboard();
this.state = {
widgets: [],
filteredDashboards: [],
dashboards: [],
- selectedDashboard: dashboard,
+ selectedDashboard: null,
isDashboardSelectorOpen: false,
isNewDashboardOpen: false,
isWidgetWizardOpen: false,
isEditModalOpen: false,
editWidgetData: {},
- dashboardInputValue: dashboard?.title || '',
+ dashboardInputValue: '',
filterValueDashboard: ''
};
- props.eventEmitter.on('projectChange', () => {
- this.clearDashboards();
- this.getDashboards();
+ props.eventEmitter.on('projectChange', (value) => {
+ this.getDashboards(value);
+ this.getDefaultDashboard(value);
});
}
- getDefaultDashboard() {
- let project = getActiveProject();
- if (project && project.defaultDashboard) {
- console
- const dashboard = JSON.stringify(project.defaultDashboard)
- localStorage.setItem('dashboard', dashboard);
- return project.defaultDashboard;
+ sync_context = () => {
+ // Active dashboard
+ const { activeDashboard } = this.context;
+ const { selectedDashboard } = this.state;
+ const paramDash = this.props.params?.dashboard_id;
+ let updatedDash = undefined;
+ // API call to update context
+ if ( paramDash && activeDashboard?.id !== paramDash) {
+ HttpClient.get([Settings.serverUrl, 'dashboard', paramDash])
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ const { setActiveDashboard } = this.context;
+ setActiveDashboard(data);
+ updatedDash = data;
+ this.setState({
+ selectedDashboard: data,
+ isDashboardSelectorOpen: false,
+ filterValueDashboard: '',
+ dashboardInputValue: data.title,
+ }); // callback within class component won't have updated context
+ // TODO don't pass value when converting to functional component
+ this.getWidgets(data);
+ });
}
- else {
- return null;
+
+ if (updatedDash && !selectedDashboard ) {
+ this.setState({
+ selectedDashboard: updatedDash,
+ dashboardInputValue: updatedDash.title
+ })
}
}
- clearDashboards() {
- localStorage.removeItem('dashboard');
- this.setState({
- selectedDashboard: null,
- filteredDashboards: [],
- dashboardInputValue: '',
- filterValueDashboard: ''
- });
- }
+ getDashboards = (handledOject = null) => {
+ // value is checked because of handler scope not seeing context state updates
+ // TODO: react-router loaders would be way better
+ const { primaryObject } = this.context;
+ const paramProject = this.props.params?.project_id;
+ const primaryObjectId = handledOject?.id ?? primaryObject?.id ?? paramProject;
- getDashboards() {
- let project = getActiveProject();
- if (!project) {
+ if (!primaryObjectId) {
this.setState({dashboardInputValue: ''})
return;
}
+ // TODO: set based on primaryType
let params = {
- 'project_id': project.id,
+ 'project_id': primaryObjectId,
'pageSize': 10
};
@@ -116,23 +134,60 @@ export class Dashboard extends React.Component {
HttpClient.get([Settings.serverUrl, 'dashboard'], params)
.then(response => HttpClient.handleResponse(response))
.then(data => {
- this.setState({dashboards: data['dashboards'], filteredDashboards: data['dashboards']}, this.getWidgets);
+ this.setState({dashboards: data['dashboards'], filteredDashboards: data['dashboards']});
});
}
- getWidgets() {
+ getDefaultDashboard = (handledObject = null) => {
+ const { primaryObject, activeDashboard, setActiveDashboard } = this.context;
+ const paramProject = this.props.params?.project_id;
+
+ let targetObject = handledObject ?? primaryObject ?? paramProject;
+
+ if (typeof(targetObject) === 'string') {
+ HttpClient.get([Settings.serverUrl, 'project', paramProject])
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ targetObject = data;
+ });
+
+ }
+
+ if ( !activeDashboard && targetObject?.defaultDashboard ){
+ setActiveDashboard(targetObject.defaultDashboard);
+ this.setState({
+ 'selectedDashboard': targetObject.defaultDashboard,
+ 'dashboardInputValue': targetObject.defaultDashboard?.title
+ })
+ } else {
+ this.setState({
+ 'selectedDashboard': 'Select a dashboard',
+ 'dashboardInputValue': 'Select a dashboard'
+ })
+ }
+ }
+
+ getWidgets = (dashboard) => {
let params = {'type': 'widget'};
- let dashboard = getActiveDashboard() || this.getDefaultDashboard();
- if (!dashboard) {
+ const { activeDashboard } = this.context;
+ // TODO don't pass value when converting to functional component
+ let target_dash = null;
+ if (dashboard === undefined) {
+ target_dash = activeDashboard;
+ } else {
+ target_dash = dashboard;
+ }
+ if (!target_dash) {
return;
}
- params['filter'] = 'dashboard_id=' + dashboard.id;
+ params['filter'] = 'dashboard_id=' + target_dash.id;
HttpClient.get([Settings.serverUrl, 'widget-config'], params)
.then(response => HttpClient.handleResponse(response))
.then(data => {
// set the widget project param
+ // TODO: set based on primaryType
data.widgets.forEach(widget => {
- widget.params['project'] = dashboard.project_id;
+ widget.params['project'] = target_dash.project_id;
});
this.setState({widgets: data.widgets});
});
@@ -143,23 +198,34 @@ export class Dashboard extends React.Component {
};
onDashboardSelect = (_event, value) => {
- const dashboard = JSON.stringify(value);
- localStorage.setItem('dashboard', dashboard);
+ const { setActiveDashboard } = this.context;
+ setActiveDashboard(value);
this.setState({
selectedDashboard: value,
isDashboardSelectorOpen: false,
filterValueDashboard: '',
dashboardInputValue: value.title,
- }, this.getWidgets);
+ }); // callback within class component won't have updated context
+ // TODO don't pass value when converting to functional component
+ this.getWidgets(value);
+
+ // does it really matter whether I read from params or the context here?
+ // they should be the same, reading from params 'feels' better
+ this.props.navigate('/project/' + this.props.params?.project_id + '/dashboard/' + value?.id)
};
onDashboardClear = () => {
- localStorage.removeItem('dashboard');
+ const { setActiveDashboard } = this.context;
+ setActiveDashboard();
this.setState({
- selectedDashboard: null,
- dashboardInputValue: '',
+ selectedDashboard: 'Select a dashboard',
+ dashboardInputValue: 'Select a dashboard',
filterValueDashboard: ''
- }, this.getDashboards, this.getWidgets);
+ });
+ // TODO convert to functional component and rely on context updating within callbacks
+ this.getWidgets(null);
+
+ this.props.navigate('/project/' + this.props.params?.project_id + '/dashboard/')
}
onTextInputChange = (_event, value) => {
@@ -200,12 +266,12 @@ export class Dashboard extends React.Component {
}
onDeleteDashboard = () => {
- const dashboard = getActiveDashboard();
+ const { activeDashboard, setActiveDashboard } = this.context;
- HttpClient.delete([Settings.serverUrl, 'dashboard', dashboard.id])
+ HttpClient.delete([Settings.serverUrl, 'dashboard', activeDashboard.id])
.then(response => HttpClient.handleResponse(response))
.then(() => {
- localStorage.removeItem('dashboard');
+ setActiveDashboard();
this.getDashboards();
this.setState({
isDeleteDashboardOpen: false,
@@ -224,9 +290,10 @@ export class Dashboard extends React.Component {
}
onEditWidgetSave = (editWidget) => {
- const project = getActiveProject();
- if (!editWidget.project_id && project) {
- editWidget.project_id = project.id;
+ const { primaryObject } = this.context;
+ // TODO: handle based on primaryType
+ if (!editWidget.project_id && primaryObject) {
+ editWidget.project_id = primaryObject.id;
}
this.setState({isEditModalOpen: false});
editWidget.id = this.state.currentWidgetId
@@ -271,16 +338,19 @@ export class Dashboard extends React.Component {
}
onNewWidgetSave = (newWidget) => {
- const project = getActiveProject();
- if (!newWidget.project_id && project) {
- newWidget.project_id = project.id;
+ const { primaryObject } = this.context;
+ // TODO: handle based on primaryType
+ if (!newWidget.project_id && primaryObject) {
+ newWidget.project_id = primaryObject.id;
}
HttpClient.post([Settings.serverUrl, 'widget-config'], newWidget).then(() => { this.getWidgets() });
this.setState({isWidgetWizardOpen: false});
}
componentDidMount() {
+ this.sync_context();
this.getDashboards();
+ this.getDefaultDashboard();
this.getWidgets();
}
@@ -307,9 +377,8 @@ export class Dashboard extends React.Component {
render() {
document.title = 'Dashboard | Ibutsu';
- const { widgets } = this.state || this.getWidgets();
- const project = getActiveProject();
- const dashboard = getActiveDashboard() || this.getDefaultDashboard();
+ const { widgets } = this.state;
+ const { primaryObject, activeDashboard } = this.context;
const toggle = toggleRef => (
@@ -411,7 +480,7 @@ export class Dashboard extends React.Component {
aria-label="Delete dashboard"
variant="plain"
title="Delete dashboard"
- isDisabled={!dashboard}
+ isDisabled={!activeDashboard}
onClick={this.onDeleteDashboardClick}
>
@@ -434,7 +503,7 @@ export class Dashboard extends React.Component {
- {!!project && !!dashboard && !!widgets &&
+ {!!primaryObject && !!activeDashboard && !!widgets &&
{widgets.map(widget => {
if (KNOWN_WIDGETS.includes(widget.widget)) {
@@ -529,7 +598,7 @@ export class Dashboard extends React.Component {
})}
}
- {!project &&
+ {!primaryObject &&
} headingLevel="h4" />
@@ -538,7 +607,7 @@ export class Dashboard extends React.Component {
}
- {!!project && !dashboard &&
+ {!!primaryObject && !activeDashboard &&
} headingLevel="h4" />
@@ -550,7 +619,7 @@ export class Dashboard extends React.Component {
}
- {(!!project && !!dashboard && widgets.length === 0) &&
+ {(!!primaryObject && !!activeDashboard && widgets.length === 0) &&
} headingLevel="h4" />
@@ -564,13 +633,13 @@ export class Dashboard extends React.Component {
}
-
-
- Administration
-
-
-
-
- );
- }
-}
+const AdminHome = () => {
+ return (
+
+
+
+ Administration
+
+
+
+
+ );
+};
+
+AdminHome.propTypes = {};
+
+export default AdminHome;
diff --git a/frontend/src/pages/admin/portal-edit.js b/frontend/src/pages/admin/portal-edit.js
new file mode 100644
index 00000000..211b8e39
--- /dev/null
+++ b/frontend/src/pages/admin/portal-edit.js
@@ -0,0 +1,483 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ ActionGroup,
+ Alert,
+ Button,
+ Card,
+ CardBody,
+ Form,
+ FormGroup,
+ FormHelperText,
+ HelperText,
+ HelperTextItem,
+ MenuToggle,
+ PageSection,
+ PageSectionVariants,
+ Select,
+ SelectList,
+ SelectOption,
+ TextInput,
+ TextInputGroup,
+ TextInputGroupMain,
+ TextInputGroupUtilities,
+ Title
+} from '@patternfly/react-core';
+import { Link } from 'react-router-dom';
+
+import { TimesIcon } from '@patternfly/react-icons';
+
+import { HttpClient } from '../../services/http';
+import { Settings } from '../../settings';
+import { dashboardToOption } from '../../utilities.js';
+
+
+function userToOption(user) {
+ if (!user) {
+ return '';
+ }
+ return {
+ user: user,
+ toString: function () { return this.user.name; },
+ compareTo: function (value) {
+ if (value.user) {
+ return this.user.id === value.user.id;
+ }
+ return this.user.name.toLowerCase().includes(value.toLowerCase()) ||
+ this.user.email.includes(value.toLowerCase());
+ }
+ };
+}
+
+export class PortalEdit extends React.Component {
+ static propTypes = {
+ params: PropTypes.object,
+ location: PropTypes.object,
+ navigate: PropTypes.func,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ id: props.params.id,
+ portal: {},
+ isOwnerOpen: false,
+ selectedOwner: {},
+ filterValueOwner: '',
+ filteredUsers: [],
+ inputValueOwner: '',
+ filteredDashboards: [],
+ dashboards: [],
+ isDashboardOpen: false,
+ selectedDashboard: null,
+ filterValueDashboard: '',
+ inputValueDashboard: '',
+ };
+ }
+
+ onPortalNameChanged = (value) => {
+ const { portal } = this.state;
+ portal.name = value;
+ this.setState({portal});
+ }
+
+ onPortalTitleChanged = (value) => {
+ const { portal } = this.state;
+ portal.title = value;
+ this.setState({portal});
+ }
+
+ onSubmitClick = () => {
+ const { portal, selectedOwner, selectedDashboard } = this.state;
+ portal.owner_id = selectedOwner ? selectedOwner.id : null;
+ portal.default_dashboard_id = selectedDashboard ? selectedDashboard.id : null;
+ delete portal.owner;
+ // delete portal.defaultDashboard;
+ this.savePortal(portal.id || null, portal)
+ .then(() => this.props.navigate(-1))
+ .catch((error) => console.error(error));
+ };
+
+ onOwnerToggle = () => {
+ this.setState({isOwnerOpen: !this.state.isOwnerOpen});
+ };
+
+ onDashboardToggle = () => {
+ this.setState({isDashboardOpen: !this.state.isDashboardOpen});
+ };
+
+ onOwnerInputChange = (_event, value) => {
+ this.setState({inputValueOwner: value});
+ this.setState({filterValueOwner: value});
+ };
+
+ onOwnerSelect = (event, value) => {
+ this.setState({
+ selectedOwner: value.user,
+ isOwnerOpen: false,
+ filterValueOwner: '',
+ inputValueOwner: value.user.name
+ });
+ };
+
+ onOwnerClear = () => {
+ this.setState({
+ selectedOwner: null,
+ inputValueOwner: '',
+ filterValueOwner: ''
+ });
+ }
+
+ getPortal(portalId) {
+ HttpClient.get([Settings.serverUrl, 'admin', 'portal', portalId])
+ .then(response => {
+ response = HttpClient.handleResponse(response, 'response');
+ return response.json();
+ })
+ .then(portal => {
+ this.setState({portal: portal, selectedOwner: portal.owner,
+ inputValueOwner: portal.owner?.name,
+ selectedDashboard: portal.defaultDashboard,
+ inputValueDashboard: portal.defaultDashboard?.title});
+ })
+ .catch(error => console.error(error));
+ }
+
+ onDashboardToggle = () => {
+ this.setState({isDashboardOpen: !this.state.isDashboardOpen});
+ };
+
+ onDashboardSelect = (event, value) => {
+ this.setState({
+ selectedDashboard: value.dashboard,
+ isDashboardOpen: false,
+ filterValueDashboard: '',
+ inputValueDashboard: value.dashboard.title
+ });
+ };
+
+ onDashboardClear = () => {
+ this.setState({
+ selectedDashboard: null,
+ inputValueDashboard: '',
+ filterValueDashboard: ''
+ });
+ }
+
+ onDashboardInputChange = (_event, value) => {
+ this.setState({inputValueDashboard: value});
+ this.setState({filterValueDashboard: value});
+ };
+
+ getDashboards() {
+ if (!this.state.id || this.state.id == "new" || !this.state.portal) {
+ return;
+ }
+ let params = {
+ 'portal_id': this.state.id,
+ 'pageSize': 10
+ };
+ HttpClient.get([Settings.serverUrl, 'dashboard'], params)
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => this.setState({dashboards: data['dashboards'], filteredDashboards: data['dashboards']}));
+ }
+
+ getUsers() {
+ HttpClient.get([Settings.serverUrl, 'admin', 'user'])
+ .then(response => {
+ response = HttpClient.handleResponse(response, 'response');
+ return response.json();
+ })
+ .then(data => this.setState({users: data.users, filteredUsers: data.users}))
+ .catch(error => console.error(error));
+ }
+
+ savePortal(portalId, portal) {
+ let request = null;
+ if (!portalId) {
+ request = HttpClient.post([Settings.serverUrl, 'admin', 'portal'], portal);
+ }
+ else {
+ request = HttpClient.put([Settings.serverUrl, 'admin', 'portal', portalId], {}, portal);
+ }
+ return request.then(response => HttpClient.handleResponse(response, 'response'))
+ .then(response => response.json());
+ }
+
+ componentDidMount() {
+ if (this.state.id === 'new') {
+ this.setState({portal: {title: 'New portal', name: 'new-portal'}});
+ }
+ else {
+ this.getPortal(this.state.id);
+ this.getDashboards();
+ }
+ this.getUsers();
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (
+ prevState.filterValueDashboard !== this.state.filterValueDashboard
+ ) {
+ let newSelectOptionsDashboard = this.state.dashboards;
+ if (this.state.inputValueDashboard) {
+ newSelectOptionsDashboard = this.state.dashboards.filter(menuItem =>
+ String(menuItem.title).toLowerCase().includes(this.state.filterValueDashboard.toLowerCase())
+ );
+ if (newSelectOptionsDashboard.length === 0) {
+ newSelectOptionsDashboard = [{
+ isDisabled: true,
+ value: {},
+ title: `No results found for "${this.state.filterValueDashboard}"`,
+ }];
+ }
+
+ if (!this.state.isDashboardOpen) {
+ this.setState({ isDashboardOpen: true });
+ }
+ }
+
+ this.setState({
+ filteredDashboards: newSelectOptionsDashboard,
+ });
+ }
+
+ if (
+ prevState.filterValueOwner !== this.state.filterValueOwner
+ ) {
+ let newSelectOptionsUser = this.state.users;
+ if (this.state.inputValueOwner) {
+ newSelectOptionsUser = this.state.users.filter(menuItem =>
+ String(menuItem.name).toLowerCase().includes(this.state.filterValueOwner.toLowerCase())
+ );
+ if (newSelectOptionsUser.length === 0) {
+ newSelectOptionsUser = [{
+ isDisabled: true,
+ value: {},
+ name: `No results found for "${this.state.filterValueOwner}"`,
+ }];
+ }
+
+ if (!this.state.isOwnerOpen) {
+ this.setState({ isOwnerOpen: true });
+ }
+ }
+
+ this.setState({
+ filteredUsers: newSelectOptionsUser,
+ });
+ }
+ }
+
+
+ render() {
+ const { portal, filteredUsers, selectedOwner, filteredDashboards, selectedDashboard, inputValueDashboard, inputValueOwner } = this.state;
+
+ const toggleOwner = toggleRef => (
+
+
+
+
+ {(!!inputValueOwner) && (
+
+ )}
+
+
+
+ )
+
+ const toggleDashboard = toggleRef => (
+
+
+
+
+ {(!!inputValueDashboard) && (
+
+ )}
+
+
+
+ )
+
+ return (
+
+
+
+ Portals / {portal && portal.title}
+
+
+
+ {!portal && }
+ {portal &&
+
+
+
+
+
+ }
+
+
+ );
+ }
+}
diff --git a/frontend/src/pages/admin/portal-list.js b/frontend/src/pages/admin/portal-list.js
new file mode 100644
index 00000000..07cde110
--- /dev/null
+++ b/frontend/src/pages/admin/portal-list.js
@@ -0,0 +1,258 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ Button,
+ Card,
+ CardBody,
+ Flex,
+ FlexItem,
+ Modal,
+ PageSection,
+ PageSectionVariants,
+ Text,
+ TextContent,
+ TextInput
+} from '@patternfly/react-core';
+import { PencilAltIcon, PlusCircleIcon, TrashIcon } from '@patternfly/react-icons';
+import { Link } from 'react-router-dom';
+
+import { HttpClient } from '../../services/http';
+import { Settings } from '../../settings';
+import { debounce, getSpinnerRow } from '../../utilities';
+import { FilterTable } from '../../components';
+
+function portalToRow(portal, onDeleteClick) {
+ return {
+ cells: [
+ {title: portal.title},
+ {title: portal.name},
+ {title: portal.owner && portal.owner.name},
+ {
+ title: (
+
+
+
+
+
+ )
+ }
+ ]
+ }
+}
+
+export class PortalList extends React.Component {
+ static propTypes = {
+ location: PropTypes.object,
+ navigate: PropTypes.func,
+ }
+
+ constructor(props) {
+ super(props);
+ const params = new URLSearchParams(props.location.search);
+ let page = 1, pageSize = 20;
+ if (params.toString() !== '') {
+ for(let pair of params) {
+ if (pair[0] === 'page') {
+ page = parseInt(pair[1]);
+ }
+ else if (pair[0] === 'pageSize') {
+ pageSize = parseInt(pair[1]);
+ }
+ }
+ }
+ this.state = {
+ columns: ['Title', 'Name', 'Owner', ''],
+ rows: [getSpinnerRow(4)],
+ portals: [],
+ page: page,
+ pageSize: pageSize,
+ totalItems: 0,
+ totalPages: 0,
+ isError: false,
+ isEmpty: false,
+ selectedPortal: null,
+ isDeleting: false,
+ isDeleteModalOpen: false,
+ textFilter: ''
+ };
+ }
+
+ updateUrl() {
+ let params = [];
+ params.push('page=' + this.state.page);
+ params.push('pageSize=' + this.state.pageSize);
+ this.props.navigate('/admin/portals?' + params.join('&'));
+ }
+
+ setPage = (_event, pageNumber) => {
+ this.setState({page: pageNumber}, () => {
+ this.updateUrl();
+ });
+ }
+
+ setPageSize = (_event, perPage) => {
+ this.setState({pageSize: perPage}, () => {
+ this.updateUrl();
+ });
+ }
+
+ getPortals() {
+ // distract the user with jingling keys
+ this.setState({rows: [getSpinnerRow(4)], isEmpty: false, isError: false});
+ let params = {
+ pageSize: this.state.pageSize,
+ page: this.state.page
+ };
+ if (this.state.textFilter) {
+ params['filter'] = ['title%' + this.state.textFilter];
+ }
+ HttpClient.get([Settings.serverUrl, 'admin', 'portal'], params)
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => this.setState({
+ rows: data.portals.map((portal) => portalToRow(portal, this.onDeleteClick)),
+ portals: data.portals,
+ page: data.pagination.page,
+ pageSize: data.pagination.pageSize,
+ totalItems: data.pagination.totalItems,
+ totalPages: data.pagination.totalPages,
+ isEmpty: data.pagination.totalItems === 0
+ }))
+ .catch((error) => {
+ console.error('Error fetching portals data:', error);
+ this.setState({rows: [], isEmpty: false, isError: true});
+ });
+ }
+
+ onDeleteClick = (portalId) => {
+ const selectedPortal = this.state.portals.find((portal) => portal.id === portalId);
+ this.setState({selectedPortal: selectedPortal, isDeleteModalOpen: true});
+ };
+
+ onDeleteModalClose = () => {
+ this.setState({isDeleteModalOpen: false});
+ };
+
+ onModalDeleteClick = () => {
+ // spinner
+ HttpClient.delete([Settings.serverUrl, 'admin', 'portal', this.state.selectedPortal.id])
+ .then(response => HttpClient.handleResponse(response))
+ .then(() => {
+ this.getPortals();
+ this.setState({isDeleteModalOpen: false});
+ });
+ }
+
+ onTextChanged = (newValue) => {
+ this.setState({textFilter: newValue}, debounce(() => {
+ if (newValue.length >= 3 || newValue.length === 0) {
+ this.updateUrl();
+ this.getPortals();
+ }
+ }));
+ };
+
+ componentDidMount() {
+ this.getPortals();
+ }
+
+ render() {
+ document.title = 'Portals - Administration | Ibutsu';
+ const { columns, rows, textFilter } = this.state;
+ const pagination = {
+ pageSize: this.state.pageSize,
+ page: this.state.page,
+ totalItems: this.state.totalItems
+ };
+ const filters = [
+ this.onTextChanged(newValue)} style={{height: "inherit"}} key="textFilter"/>
+ ];
+ return (
+
+
+
+
+
+
+ Portals
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.state.isDeleting ? 'Deleting...' : 'Delete'}
+ ,
+
+ ]}
+ >
+ Are you sure you want to delete “{this.state.selectedPortal && this.state.selectedPortal.title}”? This cannot be undone!
+
+
+ );
+ }
+}
diff --git a/frontend/src/portal.js b/frontend/src/portal.js
new file mode 100644
index 00000000..11087c35
--- /dev/null
+++ b/frontend/src/portal.js
@@ -0,0 +1 @@
+// probably don't need this, just use app and handle selection from the masthead there?
diff --git a/frontend/src/profile.js b/frontend/src/profile.js
index 076e4386..df5b879e 100644
--- a/frontend/src/profile.js
+++ b/frontend/src/profile.js
@@ -1,49 +1,33 @@
import React from 'react';
-import {
- Nav,
- NavList
-} from '@patternfly/react-core';
-import { NavLink, Route, Routes } from 'react-router-dom';
+import { Navigate, Route, Routes } from 'react-router-dom';
import EventEmitter from 'wolfy87-eventemitter';
import { UserProfile } from './pages/profile/user';
import { UserTokens } from './pages/profile/tokens';
-import { IbutsuPage } from './components';
import './app.css';
import ElementWrapper from './components/elementWrapper';
+import ProfilePage from './components/profile-page';
-export class Profile extends React.Component {
- constructor(props) {
- super(props);
- this.eventEmitter = new EventEmitter();
- }
-
- render() {
- const navigation = (
-
- );
-
- return (
-
-
-
- } />
- } />
-
-
-
- );
- }
+const Profile = () => {
+ // TODO useEffect
+ const eventEmitter = new EventEmitter();
+
+ return (
+
+ }>
+ } />
+ } />
+ }/>
+
+
+ );
}
+
+Profile.propTypes = {
+
+};
+
+export default Profile;
diff --git a/frontend/src/report-builder.js b/frontend/src/report-builder.js
index 9481f428..36885878 100644
--- a/frontend/src/report-builder.js
+++ b/frontend/src/report-builder.js
@@ -31,10 +31,10 @@ import {
toTitleCase,
parseFilter,
getSpinnerRow,
- getActiveProject,
} from './utilities';
import { DownloadButton, FilterTable } from './components';
import { OPERATIONS } from './constants';
+import { IbutsuContext } from './services/context';
function reportToRow(report) {
@@ -64,6 +64,7 @@ function reportToRow(report) {
}
export class ReportBuilder extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
location: PropTypes.object,
eventEmitter: PropTypes.object
@@ -139,9 +140,9 @@ export class ReportBuilder extends React.Component {
pageSize: this.state.pageSize,
page: this.state.page
};
- const project = getActiveProject();
- if (project) {
- params['project'] = project.id;
+ const { primaryObject } = this.context;
+ if (primaryObject) {
+ params['project'] = primaryObject.id;
}
HttpClient.get([Settings.serverUrl, 'report'], params)
.then(response => HttpClient.handleResponse(response))
@@ -167,14 +168,14 @@ export class ReportBuilder extends React.Component {
}
onRunReportClick = () => {
- const project = getActiveProject();
+ const { primaryObject } = this.context;
let params = {
type: this.state.reportType,
filter: this.state.reportFilter,
source: this.state.reportSource
};
- if (project) {
- params['project'] = project.id;
+ if (primaryObject) {
+ params['project'] = primaryObject.id;
}
HttpClient.post([Settings.serverUrl, 'report'], params).then(() => this.getReports());
};
diff --git a/frontend/src/result-list.js b/frontend/src/result-list.js
index fdf9496b..e2dc7cc0 100644
--- a/frontend/src/result-list.js
+++ b/frontend/src/result-list.js
@@ -28,7 +28,6 @@ import { HttpClient } from './services/http';
import { Settings } from './settings';
import {
buildParams,
- getActiveProject,
getFilterMode,
getOperationMode,
getOperationsFromField,
@@ -38,8 +37,11 @@ import {
} from './utilities';
import { FilterTable, MultiValueInput } from './components';
import { OPERATIONS, RESULT_FIELDS } from './constants';
+import { IbutsuContext } from './services/context';
export class ResultList extends React.Component {
+ static contextType = IbutsuContext;
+
static propTypes = {
location: PropTypes.object,
navigate: PropTypes.func,
@@ -356,7 +358,7 @@ export class ResultList extends React.Component {
let params = buildParams(this.state.filters);
params.push('page=' + this.state.page);
params.push('pageSize=' + this.state.pageSize);
- this.props.navigate('/results?' + params.join('&'))
+ this.props.navigate('results?' + params.join('&'))
}
setPage = (_event, pageNumber) => {
@@ -378,9 +380,9 @@ export class ResultList extends React.Component {
this.setState({rows: [getSpinnerRow(5)], isEmpty: false, isError: false});
let params = {filter: []};
let filters = this.state.filters;
- const project = getActiveProject();
- if (project) {
- filters['project_id'] = {'val': project.id, 'op': 'eq'};
+ const { primaryObject } = this.context;
+ if (primaryObject) {
+ filters['project_id'] = {'val': primaryObject.id, 'op': 'eq'};
}
else if (Object.prototype.hasOwnProperty.call(filters, 'project_id')) {
delete filters['project_id']
diff --git a/frontend/src/result.js b/frontend/src/result.js
index 68a0b01e..e09b3d23 100644
--- a/frontend/src/result.js
+++ b/frontend/src/result.js
@@ -25,7 +25,7 @@ export class Result extends React.Component {
this.state = {
isResultValid: false,
testResult: null,
- id: props.params.id
+ id: props.params.result_id
};
}
diff --git a/frontend/src/run-list.js b/frontend/src/run-list.js
index 9a31b812..4e4fbc0f 100644
--- a/frontend/src/run-list.js
+++ b/frontend/src/run-list.js
@@ -28,7 +28,6 @@ import { Settings } from './settings';
import {
buildBadge,
buildParams,
- getActiveProject,
getFilterMode,
getOperationMode,
getOperationsFromField,
@@ -38,6 +37,7 @@ import {
} from './utilities';
import { MultiValueInput, FilterTable, RunSummary } from './components';
import { OPERATIONS, RUN_FIELDS } from './constants';
+import { IbutsuContext } from './services/context';
function runToRow(run, filterFunc) {
@@ -75,16 +75,18 @@ function runToRow(run, filterFunc) {
}
return {
"cells": [
- {title: {run.id} {badges}},
+ {title: {run.id} {badges}},
{title: round(run.duration) + 's'},
{title: },
{title: created.toLocaleString()},
- {title: See results }
+ {title: See results }
]
};
}
export class RunList extends React.Component {
+ static contextType = IbutsuContext;
+
static propTypes = {
location: PropTypes.object,
navigate: PropTypes.func,
@@ -136,8 +138,8 @@ export class RunList extends React.Component {
isBoolOpen: false,
};
this.params = new URLSearchParams(props.location.search);
- props.eventEmitter.on('projectChange', () => {
- this.getRuns();
+ props.eventEmitter.on('projectChange', (value) => {
+ this.getRuns(value);
});
}
@@ -265,7 +267,6 @@ export class RunList extends React.Component {
this.setState({filters: filters, page: 1}, callback);
}
-
setFilter = (field, value) => {
this.updateFilters(field, 'eq', value, () => {
this.updateUrl();
@@ -280,7 +281,6 @@ export class RunList extends React.Component {
});
}
-
removeFilter = id => {
this.updateFilters(id, null, null, () => {
this.updateUrl();
@@ -309,14 +309,15 @@ export class RunList extends React.Component {
});
}
- getRuns() {
+ getRuns = (handledOject = null) => {
// First, show a spinner
this.setState({rows: [getSpinnerRow(5)], isEmpty: false, isError: false});
let params = {filter: []};
let filters = this.state.filters;
- const project = getActiveProject();
- if (project) {
- filters['project_id'] = {'val': project.id, 'op': 'eq'};
+ const { primaryObject } = this.context;
+ const targetObject = handledOject ?? primaryObject;
+ if (targetObject) {
+ filters['project_id'] = {'val': targetObject.id, 'op': 'eq'};
}
else if (Object.prototype.hasOwnProperty.call(filters, 'project_id')) {
delete filters['project_id']
@@ -346,7 +347,7 @@ export class RunList extends React.Component {
console.error('Error fetching run data:', error);
this.setState({rows: [], isEmpty: false, isError: true});
});
- }
+ };
clearFilters = () => {
this.setState({
@@ -358,10 +359,8 @@ export class RunList extends React.Component {
textFilter: '',
inValues: [],
boolSelection: null,
- }, function () {
- this.updateUrl();
- this.getRuns();
});
+ this.updateUrl();
};
componentDidMount() {
diff --git a/frontend/src/run.js b/frontend/src/run.js
index 8a076821..c16f70a1 100644
--- a/frontend/src/run.js
+++ b/frontend/src/run.js
@@ -114,7 +114,7 @@ export class Run extends React.Component {
super(props);
this.state = {
run: MockRun,
- id: props.params.id,
+ id: props.params.run_id,
testResult: null,
columns: ['Test', 'Run', 'Result', 'Duration', 'Started'],
rows: [getSpinnerRow(5)],
@@ -236,6 +236,7 @@ export class Run extends React.Component {
}
getRunArtifacts() {
+ if (!this.state.id) {return;}
HttpClient.get([Settings.serverUrl, 'artifact'], {runId: this.state.id})
.then(response => HttpClient.handleResponse(response))
.then(data => {
@@ -356,6 +357,7 @@ export class Run extends React.Component {
}
getRun() {
+ if (!this.state.id) {return;}
HttpClient.get([Settings.serverUrl, 'run', this.state.id])
.then(response => {
response = HttpClient.handleResponse(response, 'response');
@@ -522,7 +524,7 @@ export class Run extends React.Component {
{!this.state.isRunValid &&
-
+
}
{this.state.isRunValid &&
@@ -733,7 +735,7 @@ export class Run extends React.Component {
- See all results
+ See all results
diff --git a/frontend/src/services/context.js b/frontend/src/services/context.js
new file mode 100644
index 00000000..b5153787
--- /dev/null
+++ b/frontend/src/services/context.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import {createContext, useState} from 'react';
+import PropTypes from 'prop-types';
+
+
+const IbutsuContext = createContext({primaryType: 'project'});
+
+const IbutsuContextProvider = (props) => {
+ const [primaryType, setPrimaryType] = useState();
+ const [primaryObject, setPrimaryObject] = useState();
+ const [activeDashboard, setActiveDashboard] = useState();
+
+ return (
+
+ {props.children}
+
+ );
+}
+
+IbutsuContextProvider.propTypes = {
+ children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
+}
+
+export {IbutsuContext, IbutsuContextProvider};
diff --git a/frontend/src/utilities.js b/frontend/src/utilities.js
index 453e4750..44f1d097 100644
--- a/frontend/src/utilities.js
+++ b/frontend/src/utilities.js
@@ -32,6 +32,8 @@ import {
import { ClassificationDropdown } from './components';
+
+
export function getDateString() {
return String((new Date()).getTime());
}
@@ -200,14 +202,14 @@ export function resultToRow(result, filterFunc) {
}
}
if (result.metadata && result.metadata.run) {
- runLink = {result.run_id};
+ runLink = {result.run_id};
}
if (result.metadata && result.metadata.classification) {
classification = {result.metadata.classification.split('_')[0]};
}
return {
"cells": [
- {title: {result.test_id} {markers}},
+ {title: {result.test_id} {markers}},
{title: runLink},
{title: {resultIcon} {toTitleCase(result.result)} {classification}},
{title: round(result.duration) + 's'},
@@ -247,7 +249,7 @@ export function resultToClassificationRow(result, index, filterFunc) {
"isOpen": false,
"result": result,
"cells": [
- {title: {result.test_id} {markers}},
+ {title: {result.test_id} {markers}},
{title: {resultIcon} {toTitleCase(result.result)}},
{title: {exceptionBadge}},
{title: },
@@ -282,7 +284,7 @@ export function resultToComparisonRow(result, index) {
}
let cells = []
- cells.push({title: {result[0].test_id} {markers}});
+ cells.push({title: {result[0].test_id} {markers}});
result.forEach((result, index) => {
cells.push({title: {resultIcons[index]} {toTitleCase(result.result)}});
});
@@ -401,17 +403,11 @@ export function getOperationsFromField(field) {
return operations;
}
-export function getActiveProject() {
- let project = localStorage.getItem('project');
- if (project) {
- project = JSON.parse(project);
- }
- return project;
-}
-
+// TODO remove, moved to react routing params and context
export function clearActiveProject() {
localStorage.removeItem('project');
}
+// TODO remove, moved to react routing params and context
export function getActiveDashboard() {
let dashboard = localStorage.getItem('dashboard');
@@ -420,6 +416,7 @@ export function getActiveDashboard() {
}
return dashboard;
}
+// TODO remove, moved to react routing params and context
export function clearActiveDashboard() {
localStorage.removeItem('dashboard');
@@ -527,6 +524,7 @@ export function debounce(func, timeout = 500) {
};
}
+// TODO remove, move to AppContext
export function getTheme() {
return localStorage.getItem('theme');
}
diff --git a/frontend/src/views/accessibilityanalysis.js b/frontend/src/views/accessibilityanalysis.js
index bb94ab7a..8ae3db91 100644
--- a/frontend/src/views/accessibilityanalysis.js
+++ b/frontend/src/views/accessibilityanalysis.js
@@ -32,13 +32,13 @@ import { Settings } from '../settings';
import { JSONTree } from 'react-json-tree';
import Editor from '@monaco-editor/react';
import {
- getActiveProject,
parseFilter,
getSpinnerRow,
resultToRow,
} from '../utilities';
import { GenericAreaWidget } from '../widgets';
import { FilterTable, TabTitle } from '../components';
+import { IbutsuContext } from '../services/context';
const MockRun = {
id: null,
duration: null,
@@ -54,6 +54,7 @@ const MockRun = {
export class AccessibilityAnalysisView extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
location: PropTypes.object,
navigate: PropTypes.func,
@@ -119,16 +120,16 @@ export class AccessibilityAnalysisView extends React.Component {
return;
}
let params = this.props.view.params;
- let project = getActiveProject();
- if (project) {
- params['project'] = project.id;
+ const { primaryObject } = this.context;
+ if (primaryObject) {
+ params['project'] = primaryObject.id;
}
else {
delete params['project'];
}
// probably don't need this, but maybe something similar
params["run_list"] = this.state.filters.run_list?.val;
- HttpClient.get([Settings.serverUrl + '/widget/' + this.props.view.widget], params)
+ HttpClient.get([Settings.serverUrl, 'widget', this.props.view.widget], params)
.then(response => HttpClient.handleResponse(response))
.then(data => {
this.setState({
@@ -263,7 +264,7 @@ export class AccessibilityAnalysisView extends React.Component {
if (node.result) {
this.setState({currentTest: node.result}, () => {
if (!this.state.currentTest.artifacts) {
- HttpClient.get([Settings.serverUrl + '/artifact'], {resultId: this.state.currentTest.id})
+ HttpClient.get([Settings.serverUrl, 'artifact'], {resultId: this.state.currentTest.id})
.then(response => HttpClient.handleResponse(response))
.then(data => {
let { currentTest } = this.state;
@@ -292,7 +293,7 @@ export class AccessibilityAnalysisView extends React.Component {
}
getRun() {
- HttpClient.get([Settings.serverUrl + '/run/' + this.state.id])
+ HttpClient.get([Settings.serverUrl, 'run', this.state.id])
.then(response => {
response = HttpClient.handleResponse(response, 'response');
if (response.ok) {
@@ -333,7 +334,7 @@ export class AccessibilityAnalysisView extends React.Component {
}
getResultsForPie_old() {
- HttpClient.get([Settings.serverUrl + '/widget/accessibility-bar-chart'], {run_list: this.state.id})
+ HttpClient.get([Settings.serverUrl, 'widget', 'accessibility-bar-chart'], {run_list: this.state.id})
.then(response => HttpClient.handleResponse(response))
.then(data => this.setState({
pieData: data,
diff --git a/frontend/src/views/accessibilitydashboard.js b/frontend/src/views/accessibilitydashboard.js
index 125d67e2..eb3f95f0 100644
--- a/frontend/src/views/accessibilitydashboard.js
+++ b/frontend/src/views/accessibilitydashboard.js
@@ -24,7 +24,6 @@ import { Settings } from '../settings';
import {
buildBadge,
buildParams,
- getActiveProject,
getFilterMode,
getOperationMode,
getOperationsFromField,
@@ -33,6 +32,7 @@ import {
} from '../utilities';
import { FilterTable, MultiValueInput, RunSummary } from '../components';
import { OPERATIONS, ACCESSIBILITY_FIELDS } from '../constants';
+import { IbutsuContext } from '../services/context';
function runToRow(run, filterFunc, analysisViewId) {
let badges = [];
@@ -90,6 +90,7 @@ function fieldToColumnName(fields) {
}
export class AccessibilityDashboardView extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
location: PropTypes.object,
navigate: PropTypes.func,
@@ -303,15 +304,15 @@ export class AccessibilityDashboardView extends React.Component {
let analysisViewId = '';
let params = {filter: []};
let filters = this.state.filters;
- const project = getActiveProject();
- if (project) {
- filters['project_id'] = {'val': project.id, 'op': 'eq'};
+ const { primaryObject } = this.context;
+ if (primaryObject) {
+ filters['project_id'] = {'val': primaryObject.id, 'op': 'eq'};
}
else if (Object.prototype.hasOwnProperty.call(filters, 'project_id')) {
delete filters['project_id']
}
// get the widget ID for the analysis view
- HttpClient.get([Settings.serverUrl + '/widget-config'], {"filter": "widget=accessibility-analysis-view"})
+ HttpClient.get([Settings.serverUrl, 'widget-config'], {"filter": "widget=accessibility-analysis-view"})
.then(response => HttpClient.handleResponse(response))
.then(data => {
analysisViewId = data.widgets[0]?.id
diff --git a/frontend/src/views/compareruns.js b/frontend/src/views/compareruns.js
index a53ba215..6d5871a8 100644
--- a/frontend/src/views/compareruns.js
+++ b/frontend/src/views/compareruns.js
@@ -24,13 +24,14 @@ import {
import { HttpClient } from '../services/http';
import { Settings } from '../settings';
import {
- getActiveProject,
toAPIFilter,
getSpinnerRow,
resultToComparisonRow
} from '../utilities';
+import { IbutsuContext } from '../services/context';
export class CompareRunsView extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
location: PropTypes.object,
view: PropTypes.object
@@ -132,8 +133,8 @@ export class CompareRunsView extends React.Component {
if (isNew === true) {
// Add project id to params
- let project = getActiveProject();
- let projectId = project ? project.id : ''
+ const { primaryObject } = this.context;
+ const projectId = primaryObject ? primaryObject.id : ''
filter.forEach(filter => {
filter['project_id'] = {op: 'in', val: projectId};
});
@@ -270,8 +271,9 @@ export class CompareRunsView extends React.Component {
]
+ const { primaryObject } = this.context;
// Compare runs work only when project is selected
- return ( getActiveProject() &&
+ return ( primaryObject &&
diff --git a/frontend/src/views/jenkinsjob.js b/frontend/src/views/jenkinsjob.js
index 40f4c596..babb1ce8 100644
--- a/frontend/src/views/jenkinsjob.js
+++ b/frontend/src/views/jenkinsjob.js
@@ -22,7 +22,6 @@ import { HttpClient } from '../services/http';
import { Settings } from '../settings';
import {
buildParams,
- getActiveProject,
getFilterMode,
getOperationMode,
getOperationsFromField,
@@ -31,24 +30,26 @@ import {
} from '../utilities';
import { FilterTable, MultiValueInput, RunSummary } from '../components';
import { OPERATIONS, JJV_FIELDS } from '../constants';
+import { IbutsuContext } from '../services/context';
function jobToRow(job, analysisViewId) {
let start_time = new Date(job.start_time);
return {
cells: [
- analysisViewId ? {title: {job.job_name}} : job.job_name,
+ analysisViewId ? {title: {job.job_name}} : job.job_name,
{title: {job.build_number}},
{title: },
job.source,
job.env,
start_time.toLocaleString(),
- {title: See runs }
+ {title: See runs }
]
};
}
export class JenkinsJobView extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
location: PropTypes.object,
navigate: PropTypes.func,
@@ -260,9 +261,8 @@ export class JenkinsJobView extends React.Component {
getData() {
let analysisViewId = '';
- let filters = this.state.filters;
+ const filters = this.state.filters;
let params = this.props.view.params;
- let project = getActiveProject();
// get the widget ID for the analysis view
HttpClient.get([Settings.serverUrl, 'widget-config'], {"filter": "widget=jenkins-analysis-view"})
@@ -277,8 +277,10 @@ export class JenkinsJobView extends React.Component {
if (!this.props.view) {
return;
}
- if (project) {
- params['project'] = project.id;
+
+ const { primaryObject } = this.context;
+ if (primaryObject) {
+ params['project'] = primaryObject.id;
}
else {
delete params['project'];
diff --git a/frontend/src/views/jenkinsjobanalysis.js b/frontend/src/views/jenkinsjobanalysis.js
index 7323c84c..65596b55 100644
--- a/frontend/src/views/jenkinsjobanalysis.js
+++ b/frontend/src/views/jenkinsjobanalysis.js
@@ -9,15 +9,16 @@ import {
import { HttpClient } from '../services/http';
import { Settings } from '../settings';
import {
- getActiveProject,
parseFilter,
} from '../utilities';
import { FilterHeatmapWidget, GenericAreaWidget, GenericBarWidget } from '../widgets';
import { ParamDropdown } from '../components';
import { HEATMAP_MAX_BUILDS } from '../constants'
+import { IbutsuContext } from '../services/context';
export class JenkinsJobAnalysisView extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
location: PropTypes.object,
navigate: PropTypes.func,
@@ -65,9 +66,9 @@ export class JenkinsJobAnalysisView extends React.Component {
return;
}
let params = this.props.view.params;
- let project = getActiveProject();
- if (project) {
- params['project'] = project.id;
+ const { primaryObject } = this.context;
+ if (primaryObject) {
+ params['project'] = primaryObject.id;
}
else {
delete params['project'];
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 037111a5..c1948acd 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -39,26 +39,26 @@
"@babel/highlight" "^7.24.7"
picocolors "^1.0.0"
-"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed"
- integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==
+"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.8":
+ version "7.24.9"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.9.tgz#53eee4e68f1c1d0282aa0eb05ddb02d033fc43a0"
+ integrity sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==
"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.24.7", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4"
- integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==
+ version "7.24.9"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.9.tgz#dc07c9d307162c97fa9484ea997ade65841c7c82"
+ integrity sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.24.7"
- "@babel/generator" "^7.24.7"
- "@babel/helper-compilation-targets" "^7.24.7"
- "@babel/helper-module-transforms" "^7.24.7"
- "@babel/helpers" "^7.24.7"
- "@babel/parser" "^7.24.7"
+ "@babel/generator" "^7.24.9"
+ "@babel/helper-compilation-targets" "^7.24.8"
+ "@babel/helper-module-transforms" "^7.24.9"
+ "@babel/helpers" "^7.24.8"
+ "@babel/parser" "^7.24.8"
"@babel/template" "^7.24.7"
- "@babel/traverse" "^7.24.7"
- "@babel/types" "^7.24.7"
+ "@babel/traverse" "^7.24.8"
+ "@babel/types" "^7.24.9"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
@@ -66,25 +66,25 @@
semver "^6.3.1"
"@babel/eslint-parser@^7.16.3", "@babel/eslint-parser@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz#27ebab1a1ec21f48ae191a8aaac5b82baf80d9c7"
- integrity sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.24.8.tgz#bc655255fa4ded3694cc10ef3dbea6d69639c831"
+ integrity sha512-nYAikI4XTGokU2QX7Jx+v4rxZKhKivaQaREZjuW3mrJrbdWJ5yUfohnoUULge+zEEaKjPYNxhoRgUKktjXtbwA==
dependencies:
"@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1"
eslint-visitor-keys "^2.1.0"
semver "^6.3.1"
-"@babel/generator@^7.24.7", "@babel/generator@^7.7.2":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d"
- integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==
+"@babel/generator@^7.24.8", "@babel/generator@^7.24.9", "@babel/generator@^7.7.2":
+ version "7.24.10"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.10.tgz#a4ab681ec2a78bbb9ba22a3941195e28a81d8e76"
+ integrity sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==
dependencies:
- "@babel/types" "^7.24.7"
+ "@babel/types" "^7.24.9"
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^2.5.1"
-"@babel/helper-annotate-as-pure@^7.24.7":
+"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab"
integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==
@@ -107,26 +107,26 @@
"@babel/helper-hoist-variables" "^7.12.13"
"@babel/types" "^7.12.13"
-"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz#4eb6c4a80d6ffeac25ab8cd9a21b5dfa48d503a9"
- integrity sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==
+"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz#b607c3161cd9d1744977d4f97139572fe778c271"
+ integrity sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==
dependencies:
- "@babel/compat-data" "^7.24.7"
- "@babel/helper-validator-option" "^7.24.7"
- browserslist "^4.22.2"
+ "@babel/compat-data" "^7.24.8"
+ "@babel/helper-validator-option" "^7.24.8"
+ browserslist "^4.23.1"
lru-cache "^5.1.1"
semver "^6.3.1"
-"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz#2eaed36b3a1c11c53bdf80d53838b293c52f5b3b"
- integrity sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==
+"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.24.7", "@babel/helper-create-class-features-plugin@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.8.tgz#47f546408d13c200c0867f9d935184eaa0851b09"
+ integrity sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA==
dependencies:
"@babel/helper-annotate-as-pure" "^7.24.7"
"@babel/helper-environment-visitor" "^7.24.7"
"@babel/helper-function-name" "^7.24.7"
- "@babel/helper-member-expression-to-functions" "^7.24.7"
+ "@babel/helper-member-expression-to-functions" "^7.24.8"
"@babel/helper-optimise-call-expression" "^7.24.7"
"@babel/helper-replace-supers" "^7.24.7"
"@babel/helper-skip-transparent-expression-wrappers" "^7.24.7"
@@ -175,13 +175,13 @@
dependencies:
"@babel/types" "^7.24.7"
-"@babel/helper-member-expression-to-functions@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz#67613d068615a70e4ed5101099affc7a41c5225f"
- integrity sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==
+"@babel/helper-member-expression-to-functions@^7.24.7", "@babel/helper-member-expression-to-functions@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6"
+ integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==
dependencies:
- "@babel/traverse" "^7.24.7"
- "@babel/types" "^7.24.7"
+ "@babel/traverse" "^7.24.8"
+ "@babel/types" "^7.24.8"
"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.24.7":
version "7.24.7"
@@ -191,10 +191,10 @@
"@babel/traverse" "^7.24.7"
"@babel/types" "^7.24.7"
-"@babel/helper-module-transforms@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8"
- integrity sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==
+"@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.24.9":
+ version "7.24.9"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz#e13d26306b89eea569180868e652e7f514de9d29"
+ integrity sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==
dependencies:
"@babel/helper-environment-visitor" "^7.24.7"
"@babel/helper-module-imports" "^7.24.7"
@@ -209,10 +209,10 @@
dependencies:
"@babel/types" "^7.24.7"
-"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0"
- integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878"
+ integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==
"@babel/helper-remap-async-to-generator@^7.24.7":
version "7.24.7"
@@ -255,20 +255,20 @@
dependencies:
"@babel/types" "^7.24.7"
-"@babel/helper-string-parser@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2"
- integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==
+"@babel/helper-string-parser@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d"
+ integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==
"@babel/helper-validator-identifier@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
-"@babel/helper-validator-option@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6"
- integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==
+"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d"
+ integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==
"@babel/helper-wrap-function@^7.24.7":
version "7.24.7"
@@ -280,13 +280,13 @@
"@babel/traverse" "^7.24.7"
"@babel/types" "^7.24.7"
-"@babel/helpers@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416"
- integrity sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==
+"@babel/helpers@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.8.tgz#2820d64d5d6686cca8789dd15b074cd862795873"
+ integrity sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==
dependencies:
"@babel/template" "^7.24.7"
- "@babel/types" "^7.24.7"
+ "@babel/types" "^7.24.8"
"@babel/highlight@^7.10.4", "@babel/highlight@^7.24.7":
version "7.24.7"
@@ -298,10 +298,10 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85"
- integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.24.7", "@babel/parser@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.8.tgz#58a4dbbcad7eb1d48930524a3fd93d93e9084c6f"
+ integrity sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7":
version "7.24.7"
@@ -390,6 +390,16 @@
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703"
integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==
+"@babel/plugin-proposal-private-property-in-object@^7.21.11":
+ version "7.21.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c"
+ integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.18.6"
+ "@babel/helper-create-class-features-plugin" "^7.21.0"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
+
"@babel/plugin-syntax-async-generators@^7.8.4":
version "7.8.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
@@ -609,16 +619,16 @@
"@babel/helper-plugin-utils" "^7.24.7"
"@babel/plugin-syntax-class-static-block" "^7.14.5"
-"@babel/plugin-transform-classes@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz#4ae6ef43a12492134138c1e45913f7c46c41b4bf"
- integrity sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==
+"@babel/plugin-transform-classes@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz#ad23301fe5bc153ca4cf7fb572a9bc8b0b711cf7"
+ integrity sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA==
dependencies:
"@babel/helper-annotate-as-pure" "^7.24.7"
- "@babel/helper-compilation-targets" "^7.24.7"
+ "@babel/helper-compilation-targets" "^7.24.8"
"@babel/helper-environment-visitor" "^7.24.7"
"@babel/helper-function-name" "^7.24.7"
- "@babel/helper-plugin-utils" "^7.24.7"
+ "@babel/helper-plugin-utils" "^7.24.8"
"@babel/helper-replace-supers" "^7.24.7"
"@babel/helper-split-export-declaration" "^7.24.7"
globals "^11.1.0"
@@ -631,12 +641,12 @@
"@babel/helper-plugin-utils" "^7.24.7"
"@babel/template" "^7.24.7"
-"@babel/plugin-transform-destructuring@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz#a097f25292defb6e6cc16d6333a4cfc1e3c72d9e"
- integrity sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==
+"@babel/plugin-transform-destructuring@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550"
+ integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==
dependencies:
- "@babel/helper-plugin-utils" "^7.24.7"
+ "@babel/helper-plugin-utils" "^7.24.8"
"@babel/plugin-transform-dotall-regex@^7.24.7":
version "7.24.7"
@@ -740,13 +750,13 @@
"@babel/helper-module-transforms" "^7.24.7"
"@babel/helper-plugin-utils" "^7.24.7"
-"@babel/plugin-transform-modules-commonjs@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz#9fd5f7fdadee9085886b183f1ad13d1ab260f4ab"
- integrity sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==
+"@babel/plugin-transform-modules-commonjs@^7.24.7", "@babel/plugin-transform-modules-commonjs@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c"
+ integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==
dependencies:
- "@babel/helper-module-transforms" "^7.24.7"
- "@babel/helper-plugin-utils" "^7.24.7"
+ "@babel/helper-module-transforms" "^7.24.8"
+ "@babel/helper-plugin-utils" "^7.24.8"
"@babel/helper-simple-access" "^7.24.7"
"@babel/plugin-transform-modules-systemjs@^7.24.7":
@@ -824,12 +834,12 @@
"@babel/helper-plugin-utils" "^7.24.7"
"@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
-"@babel/plugin-transform-optional-chaining@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz#b8f6848a80cf2da98a8a204429bec04756c6d454"
- integrity sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==
+"@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d"
+ integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==
dependencies:
- "@babel/helper-plugin-utils" "^7.24.7"
+ "@babel/helper-plugin-utils" "^7.24.8"
"@babel/helper-skip-transparent-expression-wrappers" "^7.24.7"
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
@@ -961,21 +971,21 @@
dependencies:
"@babel/helper-plugin-utils" "^7.24.7"
-"@babel/plugin-transform-typeof-symbol@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz#f074be466580d47d6e6b27473a840c9f9ca08fb0"
- integrity sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==
+"@babel/plugin-transform-typeof-symbol@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c"
+ integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==
dependencies:
- "@babel/helper-plugin-utils" "^7.24.7"
+ "@babel/helper-plugin-utils" "^7.24.8"
"@babel/plugin-transform-typescript@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz#b006b3e0094bf0813d505e0c5485679eeaf4a881"
- integrity sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.8.tgz#c104d6286e04bf7e44b8cba1b686d41bad57eb84"
+ integrity sha512-CgFgtN61BbdOGCP4fLaAMOPkzWUh6yQZNMr5YSt8uz2cZSSiQONCQFWqsE4NeVfOIhqDOlS9CR3WD91FzMeB2Q==
dependencies:
"@babel/helper-annotate-as-pure" "^7.24.7"
- "@babel/helper-create-class-features-plugin" "^7.24.7"
- "@babel/helper-plugin-utils" "^7.24.7"
+ "@babel/helper-create-class-features-plugin" "^7.24.8"
+ "@babel/helper-plugin-utils" "^7.24.8"
"@babel/plugin-syntax-typescript" "^7.24.7"
"@babel/plugin-transform-unicode-escapes@^7.24.7":
@@ -1010,14 +1020,14 @@
"@babel/helper-plugin-utils" "^7.24.7"
"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.7.tgz#ff067b4e30ba4a72f225f12f123173e77b987f37"
- integrity sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==
- dependencies:
- "@babel/compat-data" "^7.24.7"
- "@babel/helper-compilation-targets" "^7.24.7"
- "@babel/helper-plugin-utils" "^7.24.7"
- "@babel/helper-validator-option" "^7.24.7"
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.8.tgz#e0db94d7f17d6f0e2564e8d29190bc8cdacec2d1"
+ integrity sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==
+ dependencies:
+ "@babel/compat-data" "^7.24.8"
+ "@babel/helper-compilation-targets" "^7.24.8"
+ "@babel/helper-plugin-utils" "^7.24.8"
+ "@babel/helper-validator-option" "^7.24.8"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.7"
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.7"
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7"
@@ -1048,9 +1058,9 @@
"@babel/plugin-transform-block-scoping" "^7.24.7"
"@babel/plugin-transform-class-properties" "^7.24.7"
"@babel/plugin-transform-class-static-block" "^7.24.7"
- "@babel/plugin-transform-classes" "^7.24.7"
+ "@babel/plugin-transform-classes" "^7.24.8"
"@babel/plugin-transform-computed-properties" "^7.24.7"
- "@babel/plugin-transform-destructuring" "^7.24.7"
+ "@babel/plugin-transform-destructuring" "^7.24.8"
"@babel/plugin-transform-dotall-regex" "^7.24.7"
"@babel/plugin-transform-duplicate-keys" "^7.24.7"
"@babel/plugin-transform-dynamic-import" "^7.24.7"
@@ -1063,7 +1073,7 @@
"@babel/plugin-transform-logical-assignment-operators" "^7.24.7"
"@babel/plugin-transform-member-expression-literals" "^7.24.7"
"@babel/plugin-transform-modules-amd" "^7.24.7"
- "@babel/plugin-transform-modules-commonjs" "^7.24.7"
+ "@babel/plugin-transform-modules-commonjs" "^7.24.8"
"@babel/plugin-transform-modules-systemjs" "^7.24.7"
"@babel/plugin-transform-modules-umd" "^7.24.7"
"@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7"
@@ -1073,7 +1083,7 @@
"@babel/plugin-transform-object-rest-spread" "^7.24.7"
"@babel/plugin-transform-object-super" "^7.24.7"
"@babel/plugin-transform-optional-catch-binding" "^7.24.7"
- "@babel/plugin-transform-optional-chaining" "^7.24.7"
+ "@babel/plugin-transform-optional-chaining" "^7.24.8"
"@babel/plugin-transform-parameters" "^7.24.7"
"@babel/plugin-transform-private-methods" "^7.24.7"
"@babel/plugin-transform-private-property-in-object" "^7.24.7"
@@ -1084,7 +1094,7 @@
"@babel/plugin-transform-spread" "^7.24.7"
"@babel/plugin-transform-sticky-regex" "^7.24.7"
"@babel/plugin-transform-template-literals" "^7.24.7"
- "@babel/plugin-transform-typeof-symbol" "^7.24.7"
+ "@babel/plugin-transform-typeof-symbol" "^7.24.8"
"@babel/plugin-transform-unicode-escapes" "^7.24.7"
"@babel/plugin-transform-unicode-property-regex" "^7.24.7"
"@babel/plugin-transform-unicode-regex" "^7.24.7"
@@ -1093,7 +1103,7 @@
babel-plugin-polyfill-corejs2 "^0.4.10"
babel-plugin-polyfill-corejs3 "^0.10.4"
babel-plugin-polyfill-regenerator "^0.6.1"
- core-js-compat "^3.31.0"
+ core-js-compat "^3.37.1"
semver "^6.3.1"
"@babel/preset-flow@^7.24.7":
@@ -1142,10 +1152,10 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
-"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
- integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
+"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e"
+ integrity sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==
dependencies:
regenerator-runtime "^0.14.0"
@@ -1158,28 +1168,28 @@
"@babel/parser" "^7.24.7"
"@babel/types" "^7.24.7"
-"@babel/traverse@^7.24.7", "@babel/traverse@^7.7.2":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5"
- integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==
+"@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.7.2":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.8.tgz#6c14ed5232b7549df3371d820fbd9abfcd7dfab7"
+ integrity sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==
dependencies:
"@babel/code-frame" "^7.24.7"
- "@babel/generator" "^7.24.7"
+ "@babel/generator" "^7.24.8"
"@babel/helper-environment-visitor" "^7.24.7"
"@babel/helper-function-name" "^7.24.7"
"@babel/helper-hoist-variables" "^7.24.7"
"@babel/helper-split-export-declaration" "^7.24.7"
- "@babel/parser" "^7.24.7"
- "@babel/types" "^7.24.7"
+ "@babel/parser" "^7.24.8"
+ "@babel/types" "^7.24.8"
debug "^4.3.1"
globals "^11.1.0"
-"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.12.6", "@babel/types@^7.20.7", "@babel/types@^7.24.7", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2"
- integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==
+"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.12.6", "@babel/types@^7.20.7", "@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.24.9", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
+ version "7.24.9"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.9.tgz#228ce953d7b0d16646e755acf204f4cf3d08cc73"
+ integrity sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==
dependencies:
- "@babel/helper-string-parser" "^7.24.7"
+ "@babel/helper-string-parser" "^7.24.8"
"@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
@@ -1344,9 +1354,9 @@
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1":
- version "4.10.1"
- resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.1.tgz#361461e5cb3845d874e61731c11cfedd664d83a0"
- integrity sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==
+ version "4.11.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae"
+ integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==
"@eslint/eslintrc@^0.4.3":
version "0.4.3"
@@ -1687,9 +1697,9 @@
"@jridgewell/trace-mapping" "^0.3.25"
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
- version "1.4.15"
- resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
- integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
+ integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
version "0.3.25"
@@ -1779,10 +1789,10 @@
victory-voronoi-container "^36.9.1"
victory-zoom-container "^36.9.1"
-"@patternfly/react-core@^5.2.3", "@patternfly/react-core@^5.3.3":
- version "5.3.3"
- resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-5.3.3.tgz#06f589f5b0bb95dd231a0ca9379fa68d242899ab"
- integrity sha512-qq3j0M+Vi+Xmd+a/MhRhGgjdRh9Hnm79iA+L935HwMIVDcIWRYp6Isib/Ha4+Jk+f3Qdl0RT3dBDvr/4m6OpVQ==
+"@patternfly/react-core@^5.2.3", "@patternfly/react-core@^5.3.4":
+ version "5.3.4"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-5.3.4.tgz#84f85d3528655134cf0bcdb096f82777f0dd69b6"
+ integrity sha512-zr2yeilIoFp8MFOo0vNgI8XuM+P2466zHvy4smyRNRH2/but2WObqx7Wu4ftd/eBMYdNqmTeuXe6JeqqRqnPMQ==
dependencies:
"@patternfly/react-icons" "^5.3.2"
"@patternfly/react-styles" "^5.3.1"
@@ -1802,11 +1812,11 @@
integrity sha512-H6uBoFH3bJjD6PP75qZ4k+2TtF59vxf9sIVerPpwrGJcRgBZbvbMZCniSC3+S2LQ8DgXLnDvieq78jJzHz0hiA==
"@patternfly/react-table@^5.2.4":
- version "5.3.3"
- resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-5.3.3.tgz#866ec3d2d57d506199b6d53dbd7d01a98abb321a"
- integrity sha512-uaRmsJABvVPH8gYTh+EUcDz61knIxe9qor/VGUYDLONYBL5G3IaltwG42IsJ9jShxiwFmIPy+QARPpaadTpv5w==
+ version "5.3.4"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-5.3.4.tgz#f5e0ee5057cce7a54742ea9ca7f038aa9ca5e5ed"
+ integrity sha512-jGaiuo02scaC1HdGNHuYVRjtQCOB+vtvfbgS7nl1Y8ZcJ08wyUGhGSrEpNHfGAQ1XDSSoELAxj0cjOQwAAQw1A==
dependencies:
- "@patternfly/react-core" "^5.3.3"
+ "@patternfly/react-core" "^5.3.4"
"@patternfly/react-icons" "^5.3.2"
"@patternfly/react-styles" "^5.3.1"
"@patternfly/react-tokens" "^5.3.1"
@@ -1841,10 +1851,10 @@
resolved "https://registry.yarnpkg.com/@react-oauth/google/-/google-0.12.1.tgz#b76432c3a525e9afe076f787d2ded003fcc1bee9"
integrity sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==
-"@remix-run/router@1.16.1":
- version "1.16.1"
- resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.16.1.tgz#73db3c48b975eeb06d0006481bde4f5f2d17d1cd"
- integrity sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==
+"@remix-run/router@1.18.0":
+ version "1.18.0"
+ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.18.0.tgz#20b033d1f542a100c1d57cfd18ecf442d1784732"
+ integrity sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==
"@rollup/plugin-babel@^5.2.0":
version "5.3.1"
@@ -2180,10 +2190,18 @@
"@types/eslint" "*"
"@types/estree" "*"
-"@types/eslint@*", "@types/eslint@^7.29.0 || ^8.4.1":
- version "8.56.10"
- resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d"
- integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==
+"@types/eslint@*":
+ version "9.6.0"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.0.tgz#51d4fe4d0316da9e9f2c80884f2c20ed5fb022ff"
+ integrity sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==
+ dependencies:
+ "@types/estree" "*"
+ "@types/json-schema" "*"
+
+"@types/eslint@^7.29.0 || ^8.4.1":
+ version "8.56.11"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.11.tgz#e2ff61510a3b9454b3329fe7731e3b4c6f780041"
+ integrity sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
@@ -2199,9 +2217,9 @@
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33":
- version "4.19.3"
- resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz#e469a13e4186c9e1c0418fb17be8bc8ff1b19a7a"
- integrity sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==
+ version "4.19.5"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6"
+ integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==
dependencies:
"@types/node" "*"
"@types/qs" "*"
@@ -2272,9 +2290,9 @@
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash@^4.17.0":
- version "4.17.5"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.5.tgz#e6c29b58e66995d57cd170ce3e2a61926d55ee04"
- integrity sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==
+ version "4.17.7"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612"
+ integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==
"@types/mime@^1":
version "1.3.5"
@@ -2289,9 +2307,9 @@
"@types/node" "*"
"@types/node@*":
- version "20.14.2"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18"
- integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==
+ version "20.14.11"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.11.tgz#09b300423343460455043ddd4d0ded6ac579b74b"
+ integrity sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==
dependencies:
undici-types "~5.26.4"
@@ -2414,9 +2432,9 @@
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@types/ws@^8.5.5":
- version "8.5.10"
- resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787"
- integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==
+ version "8.5.11"
+ resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.11.tgz#90ad17b3df7719ce3e6bc32f83ff954d38656508"
+ integrity sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==
dependencies:
"@types/node" "*"
@@ -2699,10 +2717,10 @@ acorn-globals@^6.0.0:
acorn "^7.1.1"
acorn-walk "^7.1.1"
-acorn-import-assertions@^1.9.0:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac"
- integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==
+acorn-import-attributes@^1.9.5:
+ version "1.9.5"
+ resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
+ integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
acorn-jsx@^5.3.1, acorn-jsx@^5.3.2:
version "5.3.2"
@@ -2720,9 +2738,9 @@ acorn@^7.1.1, acorn@^7.4.0:
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.2.4, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0:
- version "8.11.3"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
- integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
+ version "8.12.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
+ integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
address@^1.0.1, address@^1.1.2:
version "1.2.2"
@@ -2782,14 +2800,14 @@ ajv@6.12.6, ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
uri-js "^4.2.2"
ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.0, ajv@^8.9.0:
- version "8.16.0"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4"
- integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==
+ version "8.17.1"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
+ integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
dependencies:
fast-deep-equal "^3.1.3"
+ fast-uri "^3.0.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
- uri-js "^4.4.1"
ansi-align@^2.0.0:
version "2.0.0"
@@ -2899,20 +2917,13 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
-aria-query@5.1.3:
+aria-query@5.1.3, aria-query@~5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e"
integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==
dependencies:
deep-equal "^2.0.5"
-aria-query@^5.3.0:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
- integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
- dependencies:
- dequal "^2.0.3"
-
array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f"
@@ -3012,17 +3023,7 @@ array.prototype.reduce@^1.0.6:
es-object-atoms "^1.0.0"
is-string "^1.0.7"
-array.prototype.toreversed@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba"
- integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==
- dependencies:
- call-bind "^1.0.2"
- define-properties "^1.2.0"
- es-abstract "^1.22.1"
- es-shim-unscopables "^1.0.0"
-
-array.prototype.tosorted@^1.1.3:
+array.prototype.tosorted@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc"
integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==
@@ -3123,17 +3124,17 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.0.tgz#d9b802e9bb9c248d7be5f7f5ef178dc3684e9dcc"
integrity sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==
-axe-core@=4.7.0:
- version "4.7.0"
- resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf"
- integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
+axe-core@^4.9.1:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.9.1.tgz#fcd0f4496dad09e0c899b44f6c4bb7848da912ae"
+ integrity sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==
-axobject-query@^3.2.1:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
- integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==
+axobject-query@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1"
+ integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==
dependencies:
- dequal "^2.0.3"
+ deep-equal "^2.0.5"
babel-jest@^27.4.2, babel-jest@^27.5.1:
version "27.5.1"
@@ -3395,15 +3396,15 @@ browser-process-hrtime@^1.0.0:
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
-browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.22.2, browserslist@^4.23.0:
- version "4.23.1"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96"
- integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==
+browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.23.0, browserslist@^4.23.1:
+ version "4.23.2"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.2.tgz#244fe803641f1c19c28c48c4b6ec9736eb3d32ed"
+ integrity sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==
dependencies:
- caniuse-lite "^1.0.30001629"
- electron-to-chromium "^1.4.796"
+ caniuse-lite "^1.0.30001640"
+ electron-to-chromium "^1.4.820"
node-releases "^2.0.14"
- update-browserslist-db "^1.0.16"
+ update-browserslist-db "^1.1.0"
bser@2.1.1:
version "2.1.1"
@@ -3504,10 +3505,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001629:
- version "1.0.30001632"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz#964207b7cba5851701afb4c8afaf1448db3884b6"
- integrity sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001640:
+ version "1.0.30001643"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz#9c004caef315de9452ab970c3da71085f8241dbd"
+ integrity sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==
case-sensitive-paths-webpack-plugin@^2.4.0:
version "2.4.0"
@@ -3875,7 +3876,7 @@ cookie@0.6.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
-core-js-compat@^3.31.0, core-js-compat@^3.36.1:
+core-js-compat@^3.36.1, core-js-compat@^3.37.1:
version "3.37.1"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee"
integrity sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==
@@ -4327,9 +4328,9 @@ data-view-byte-offset@^1.0.0:
is-data-view "^1.0.1"
dayjs@^1.10.4:
- version "1.11.11"
- resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e"
- integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==
+ version "1.11.12"
+ resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.12.tgz#5245226cc7f40a15bf52e0b99fd2a04669ccac1d"
+ integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==
debug@2.6.9, debug@^2.6.0:
version "2.6.9"
@@ -4458,11 +4459,6 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
-dequal@^2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
- integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
-
destroy@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
@@ -4677,10 +4673,10 @@ ejs@^3.1.6:
dependencies:
jake "^10.8.5"
-electron-to-chromium@^1.4.796:
- version "1.4.796"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.796.tgz#48dd6ff634b7f7df6313bd27aaa713f3af4a2b29"
- integrity sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==
+electron-to-chromium@^1.4.820:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.0.tgz#0d3123a9f09189b9c7ab4b5d6848d71b3c1fd0e8"
+ integrity sha512-Vb3xHHYnLseK8vlMJQKJYXJ++t4u1/qJ3vykuVrVjvdiOEhYyT1AuP4x03G8EnPmYvYOhe9T+dADTmthjRQMkA==
emittery@^0.10.2:
version "0.10.2"
@@ -4719,10 +4715,10 @@ end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
-enhanced-resolve@^5.16.0:
- version "5.17.0"
- resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz#d037603789dd9555b89aaec7eb78845c49089bc5"
- integrity sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==
+enhanced-resolve@^5.17.0:
+ version "5.17.1"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15"
+ integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -4795,7 +4791,7 @@ error-stack-parser@^2.0.6:
dependencies:
stackframe "^1.3.4"
-es-abstract@^1.17.2, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3:
+es-abstract@^1.17.2, es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3:
version "1.23.3"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0"
integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==
@@ -4879,7 +4875,7 @@ es-get-iterator@^1.1.3:
isarray "^2.0.5"
stop-iteration-iterator "^1.0.0"
-es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.19:
+es-iterator-helpers@^1.0.19:
version "1.0.19"
resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8"
integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==
@@ -4900,9 +4896,9 @@ es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.19:
safe-array-concat "^1.1.2"
es-module-lexer@^1.2.1:
- version "1.5.3"
- resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.3.tgz#25969419de9c0b1fbe54279789023e8a9a788412"
- integrity sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78"
+ integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==
es-object-atoms@^1.0.0:
version "1.0.0"
@@ -5066,26 +5062,26 @@ eslint-plugin-jest@^25.3.0:
"@typescript-eslint/experimental-utils" "^5.0.0"
eslint-plugin-jsx-a11y@^6.5.1:
- version "6.8.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2"
- integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==
+ version "6.9.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz#67ab8ff460d4d3d6a0b4a570e9c1670a0a8245c8"
+ integrity sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==
dependencies:
- "@babel/runtime" "^7.23.2"
- aria-query "^5.3.0"
- array-includes "^3.1.7"
+ aria-query "~5.1.3"
+ array-includes "^3.1.8"
array.prototype.flatmap "^1.3.2"
ast-types-flow "^0.0.8"
- axe-core "=4.7.0"
- axobject-query "^3.2.1"
+ axe-core "^4.9.1"
+ axobject-query "~3.1.1"
damerau-levenshtein "^1.0.8"
emoji-regex "^9.2.2"
- es-iterator-helpers "^1.0.15"
- hasown "^2.0.0"
+ es-iterator-helpers "^1.0.19"
+ hasown "^2.0.2"
jsx-ast-utils "^3.3.5"
language-tags "^1.0.9"
minimatch "^3.1.2"
- object.entries "^1.1.7"
- object.fromentries "^2.0.7"
+ object.fromentries "^2.0.8"
+ safe-regex-test "^1.0.3"
+ string.prototype.includes "^2.0.0"
eslint-plugin-react-hooks@^4.3.0:
version "4.6.2"
@@ -5093,28 +5089,28 @@ eslint-plugin-react-hooks@^4.3.0:
integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==
eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.34.2:
- version "7.34.2"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz#2780a1a35a51aca379d86d29b9a72adc6bfe6b66"
- integrity sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==
+ version "7.35.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz#00b1e4559896710e58af6358898f2ff917ea4c41"
+ integrity sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==
dependencies:
array-includes "^3.1.8"
array.prototype.findlast "^1.2.5"
array.prototype.flatmap "^1.3.2"
- array.prototype.toreversed "^1.1.2"
- array.prototype.tosorted "^1.1.3"
+ array.prototype.tosorted "^1.1.4"
doctrine "^2.1.0"
es-iterator-helpers "^1.0.19"
estraverse "^5.3.0"
+ hasown "^2.0.2"
jsx-ast-utils "^2.4.1 || ^3.0.0"
minimatch "^3.1.2"
object.entries "^1.1.8"
object.fromentries "^2.0.8"
- object.hasown "^1.1.4"
object.values "^1.2.0"
prop-types "^15.8.1"
resolve "^2.0.0-next.5"
semver "^6.3.1"
string.prototype.matchall "^4.0.11"
+ string.prototype.repeat "^1.0.0"
eslint-plugin-testing-library@^5.0.1:
version "5.11.1"
@@ -5291,9 +5287,9 @@ esprima@^4.0.0, esprima@^4.0.1:
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.4.0, esquery@^1.4.2:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
- integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
+ integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
dependencies:
estraverse "^5.1.0"
@@ -5511,6 +5507,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+fast-uri@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134"
+ integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==
+
fast-url-parser@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
@@ -5673,9 +5674,9 @@ for-each@^0.3.3:
is-callable "^1.1.3"
foreground-child@^3.1.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d"
- integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7"
+ integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==
dependencies:
cross-spawn "^7.0.0"
signal-exit "^4.0.1"
@@ -5903,14 +5904,15 @@ glob-to-regexp@^0.4.1:
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^10.3.10:
- version "10.4.1"
- resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2"
- integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==
+ version "10.4.5"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
+ integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^3.1.2"
minimatch "^9.0.4"
minipass "^7.1.2"
+ package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
@@ -6308,9 +6310,9 @@ import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
resolve-from "^4.0.0"
import-local@^3.0.2:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4"
- integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260"
+ integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==
dependencies:
pkg-dir "^4.2.0"
resolve-cwd "^3.0.0"
@@ -6445,11 +6447,11 @@ is-ci@^3.0.0:
ci-info "^3.2.0"
is-core-module@^2.13.0, is-core-module@^2.13.1:
- version "2.13.1"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
- integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
+ version "2.15.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea"
+ integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==
dependencies:
- hasown "^2.0.0"
+ hasown "^2.0.2"
is-data-view@^1.0.1:
version "1.0.1"
@@ -6743,18 +6745,18 @@ iterator.prototype@^1.1.2:
set-function-name "^2.0.1"
jackspeak@^3.1.2:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a"
- integrity sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
+ integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
dependencies:
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
jake@^10.8.5:
- version "10.9.1"
- resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.1.tgz#8dc96b7fcc41cb19aa502af506da4e1d56f5e62b"
- integrity sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==
+ version "10.9.2"
+ resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f"
+ integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==
dependencies:
async "^3.2.3"
chalk "^4.0.2"
@@ -7244,9 +7246,9 @@ jest@^27.4.3:
jest-cli "^27.5.1"
jiti@^1.21.0:
- version "1.21.3"
- resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.3.tgz#b2adb07489d7629b344d59082bbedb8c21c5f755"
- integrity sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==
+ version "1.21.6"
+ resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268"
+ integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==
js-sha256@^0.9.0:
version "0.9.0"
@@ -7454,9 +7456,9 @@ language-tags@^1.0.9:
language-subtag-registry "^0.3.20"
launch-editor@^2.6.0:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.1.tgz#f259c9ef95cbc9425620bbbd14b468fcdb4ffe3c"
- integrity sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.8.0.tgz#7255d90bdba414448e2138faa770a74f28451305"
+ integrity sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA==
dependencies:
picocolors "^1.0.0"
shell-quote "^1.8.1"
@@ -7657,9 +7659,9 @@ lower-case@^2.0.2:
tslib "^2.0.3"
lru-cache@^10.2.0:
- version "10.2.2"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878"
- integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==
+ version "10.4.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
+ integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lru-cache@^4.0.1:
version "4.1.5"
@@ -7759,11 +7761,16 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
braces "^3.0.3"
picomatch "^2.3.1"
-mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
+mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+"mime-db@>= 1.43.0 < 2":
+ version "1.53.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447"
+ integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==
+
mime-db@~1.33.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
@@ -7828,9 +7835,9 @@ minimatch@^5.0.1:
brace-expansion "^2.0.1"
minimatch@^9.0.4:
- version "9.0.4"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"
- integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==
+ version "9.0.5"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+ integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"
@@ -7952,9 +7959,9 @@ node-int64@^0.4.0:
integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
node-releases@^2.0.14:
- version "2.0.14"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
- integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
+ version "2.0.18"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f"
+ integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
@@ -8000,9 +8007,9 @@ nth-check@^2.0.1:
boolbase "^1.0.0"
nwsapi@^2.2.0:
- version "2.2.10"
- resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.10.tgz#0b77a68e21a0b483db70b11fad055906e867cda8"
- integrity sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==
+ version "2.2.12"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8"
+ integrity sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==
object-assign@^4.0.1, object-assign@^4.1.1:
version "4.1.1"
@@ -8015,9 +8022,9 @@ object-hash@^3.0.0:
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
object-inspect@^1.13.1, object-inspect@^1.7.0:
- version "1.13.1"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
- integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff"
+ integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==
object-is@^1.0.2, object-is@^1.1.5:
version "1.1.6"
@@ -8042,7 +8049,7 @@ object.assign@^4.1.0, object.assign@^4.1.4, object.assign@^4.1.5:
has-symbols "^1.0.3"
object-keys "^1.1.1"
-object.entries@^1.1.1, object.entries@^1.1.7, object.entries@^1.1.8:
+object.entries@^1.1.1, object.entries@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41"
integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==
@@ -8083,15 +8090,6 @@ object.groupby@^1.0.1:
define-properties "^1.2.1"
es-abstract "^1.23.2"
-object.hasown@^1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc"
- integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==
- dependencies:
- define-properties "^1.2.1"
- es-abstract "^1.23.2"
- es-object-atoms "^1.0.0"
-
object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b"
@@ -8230,6 +8228,11 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+package-json-from-dist@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00"
+ integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==
+
param-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
@@ -8700,11 +8703,11 @@ postcss-modules-values@^4.0.0:
icss-utils "^5.0.0"
postcss-nested@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c"
- integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131"
+ integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==
dependencies:
- postcss-selector-parser "^6.0.11"
+ postcss-selector-parser "^6.1.1"
postcss-nesting@^10.2.0:
version "10.2.0"
@@ -8907,10 +8910,10 @@ postcss-selector-not@^6.0.1:
dependencies:
postcss-selector-parser "^6.0.10"
-postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53"
- integrity sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==
+postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9, postcss-selector-parser@^6.1.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz#5be94b277b8955904476a2400260002ce6c56e38"
+ integrity sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
@@ -8944,12 +8947,12 @@ postcss@^7.0.35:
source-map "^0.6.1"
postcss@^8.3.5, postcss@^8.4.23, postcss@^8.4.33, postcss@^8.4.4:
- version "8.4.38"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
- integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
+ version "8.4.39"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3"
+ integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==
dependencies:
nanoid "^3.3.7"
- picocolors "^1.0.0"
+ picocolors "^1.0.1"
source-map-js "^1.2.0"
prelude-ls@^1.2.1:
@@ -9283,19 +9286,19 @@ react-refresh@^0.11.0:
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
react-router-dom@^6.22.3:
- version "6.23.1"
- resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.23.1.tgz#30cbf266669693e9492aa4fc0dde2541ab02322f"
- integrity sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==
+ version "6.25.1"
+ resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.25.1.tgz#b89f8d63fc8383ea4e89c44bf31c5843e1f7afa0"
+ integrity sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==
dependencies:
- "@remix-run/router" "1.16.1"
- react-router "6.23.1"
+ "@remix-run/router" "1.18.0"
+ react-router "6.25.1"
-react-router@6.23.1:
- version "6.23.1"
- resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.23.1.tgz#d08cbdbd9d6aedc13eea6e94bc6d9b29cb1c4be9"
- integrity sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==
+react-router@6.25.1:
+ version "6.25.1"
+ resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.25.1.tgz#70b4f1af79954cfcfd23f6ddf5c883e8c904203e"
+ integrity sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==
dependencies:
- "@remix-run/router" "1.16.1"
+ "@remix-run/router" "1.18.0"
react-scripts@^5.0.1:
version "5.0.1"
@@ -9616,9 +9619,9 @@ reusify@^1.0.4:
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rfdc@^1.3.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f"
- integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
+ integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
@@ -9793,9 +9796,9 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4:
- version "7.6.2"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
- integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
+ version "7.6.3"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
+ integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
send@0.18.0:
version "0.18.0"
@@ -10206,6 +10209,14 @@ string-width@^5.0.1, string-width@^5.1.2:
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
+string.prototype.includes@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f"
+ integrity sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+
string.prototype.matchall@^4.0.11, string.prototype.matchall@^4.0.6:
version "4.0.11"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a"
@@ -10224,6 +10235,14 @@ string.prototype.matchall@^4.0.11, string.prototype.matchall@^4.0.6:
set-function-name "^2.0.2"
side-channel "^1.0.6"
+string.prototype.repeat@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a"
+ integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+
string.prototype.trim@^1.2.1, string.prototype.trim@^1.2.9:
version "1.2.9"
resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4"
@@ -10457,9 +10476,9 @@ table@^6.0.9:
strip-ansi "^6.0.1"
tailwindcss@^3.0.2:
- version "3.4.4"
- resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.4.tgz#351d932273e6abfa75ce7d226b5bf3a6cb257c05"
- integrity sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==
+ version "3.4.6"
+ resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.6.tgz#41faae16607e0916da1eaa4a3b44053457ba70dd"
+ integrity sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==
dependencies:
"@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
@@ -10536,9 +10555,9 @@ terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.10:
terser "^5.26.0"
terser@^5.0.0, terser@^5.10.0, terser@^5.26.0:
- version "5.31.1"
- resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.1.tgz#735de3c987dd671e95190e6b98cfe2f07f3cf0d4"
- integrity sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==
+ version "5.31.3"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.3.tgz#b24b7beb46062f4653f049eea4f0cd165d0f0c38"
+ integrity sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA==
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2"
@@ -10594,9 +10613,9 @@ thunky@^1.0.2:
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
tlds@^1.199.0:
- version "1.252.0"
- resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.252.0.tgz#71d9617f4ef4cc7347843bee72428e71b8b0f419"
- integrity sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==
+ version "1.254.0"
+ resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.254.0.tgz#7131955376f7c195d5edc0a94b4a203bc36668d0"
+ integrity sha512-YY4ei7K7gPGifqNSrfMaPdqTqiHcwYKUJ7zhLqQOK2ildlGgti5TSwJiXXN1YqG17I2GYZh5cZqv2r5fwBUM+w==
tmp@~0.2.1:
version "0.2.3"
@@ -10881,10 +10900,10 @@ upath@^1.2.0:
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
-update-browserslist-db@^1.0.16:
- version "1.0.16"
- resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356"
- integrity sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==
+update-browserslist-db@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e"
+ integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==
dependencies:
escalade "^3.1.2"
picocolors "^1.0.1"
@@ -10897,7 +10916,7 @@ update-check@1.5.2:
registry-auth-token "3.3.2"
registry-url "3.1.0"
-uri-js@^4.2.2, uri-js@^4.4.1:
+uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
@@ -11311,9 +11330,9 @@ webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.64.4:
- version "5.91.0"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.91.0.tgz#ffa92c1c618d18c878f06892bbdc3373c71a01d9"
- integrity sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==
+ version "5.93.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.93.0.tgz#2e89ec7035579bdfba9760d26c63ac5c3462a5e5"
+ integrity sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^1.0.5"
@@ -11321,10 +11340,10 @@ webpack@^5.64.4:
"@webassemblyjs/wasm-edit" "^1.12.1"
"@webassemblyjs/wasm-parser" "^1.12.1"
acorn "^8.7.1"
- acorn-import-assertions "^1.9.0"
+ acorn-import-attributes "^1.9.5"
browserslist "^4.21.10"
chrome-trace-event "^1.0.2"
- enhanced-resolve "^5.16.0"
+ enhanced-resolve "^5.17.0"
es-module-lexer "^1.2.1"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -11696,9 +11715,9 @@ ws@^7.4.6:
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
ws@^8.13.0:
- version "8.17.1"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
- integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
+ version "8.18.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
+ integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
xml-name-validator@^3.0.0:
version "3.0.0"
diff --git a/scripts/ibutsu-pod.sh b/scripts/ibutsu-pod.sh
index 1584b4eb..8c33057e 100755
--- a/scripts/ibutsu-pod.sh
+++ b/scripts/ibutsu-pod.sh
@@ -190,7 +190,7 @@ podman run -d \
-w /mnt \
-v./frontend:/mnt/:Z \
node:18 \
- /bin/bash -c "npm install --no-save --no-package-lock yarn &&
+ /bin/bash -c "node --dns-result-order=ipv4first /usr/bin/npm install --no-save --no-package-lock yarn &&
yarn install &&
CI=1 yarn devserver"
echo "done."