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 && + + +
+ + this.onPortalTitleChanged(value)} + /> + + + The portal‘s friendly name + + + + + this.onPortalNameChanged(value)} + /> + + + The portal‘s machine name + + + + + + + + The user who owns the Portal + + + + + + + + The default dashboard for the 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."