diff --git a/CHANGELOG.md b/CHANGELOG.md index d123b651..b0382d3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Add options to customize table columns in AlertGroup page ([3281](https://github.com/grafana/oncall/pull/3281)) + ### Fixed - User profile UI tweaks ([#3443](https://github.com/grafana/oncall/pull/3443)) diff --git a/engine/apps/api/alert_group_table_columns.py b/engine/apps/api/alert_group_table_columns.py new file mode 100644 index 00000000..8fb33811 --- /dev/null +++ b/engine/apps/api/alert_group_table_columns.py @@ -0,0 +1,31 @@ +import typing + +from apps.user_management.constants import AlertGroupTableColumns, default_columns + +if typing.TYPE_CHECKING: + from apps.user_management.models import User + + +def alert_group_table_user_settings(user: "User") -> AlertGroupTableColumns: + """ + Returns user settings for alert group table columns. The flag "default" shows that user has default settings for + visible columns. It's used by frontend to enable/disable `reset` button. + This function uses lazy update to update columns settings for organization and for user. + """ + default_organization_columns = default_columns() + if not user.organization.alert_group_table_columns: + user.organization.update_alert_group_table_columns(default_organization_columns) + organization_columns = user.organization.alert_group_table_columns + if user.alert_group_table_selected_columns: + visible_columns = [ + column for column in user.alert_group_table_selected_columns if column in organization_columns + ] + else: + visible_columns = default_organization_columns + user.update_alert_group_table_selected_columns(visible_columns) + hidden_columns = [column for column in organization_columns if column not in visible_columns] + return { + "visible": visible_columns, + "hidden": hidden_columns, + "default": visible_columns == default_organization_columns, + } diff --git a/engine/apps/api/serializers/alert_group_table_settings.py b/engine/apps/api/serializers/alert_group_table_settings.py new file mode 100644 index 00000000..24dccf1a --- /dev/null +++ b/engine/apps/api/serializers/alert_group_table_settings.py @@ -0,0 +1,60 @@ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from apps.user_management.constants import ( + AlertGroupTableColumnTypeChoices, + AlertGroupTableDefaultColumnChoices, + default_columns, +) + + +class AlertGroupTableColumnSerializer(serializers.Serializer): + name = serializers.CharField(max_length=200) + id = serializers.CharField(max_length=200) + type = serializers.ChoiceField(choices=AlertGroupTableColumnTypeChoices.choices) + + def validate(self, data): + self._validate_id(data) + return data + + def _validate_id(self, data): + """Validate if `id` of column with `default` type is in the list of available default columns""" + if ( + data["type"] == AlertGroupTableColumnTypeChoices.DEFAULT.value + and data["id"] not in AlertGroupTableDefaultColumnChoices.values + ): + raise ValidationError("Invalid column id format") + + +class AlertGroupTableColumnsOrganizationSerializer(serializers.Serializer): + visible = AlertGroupTableColumnSerializer(many=True) + hidden = AlertGroupTableColumnSerializer(many=True) + + def validate(self, data): + """ + Validate that at least one column is selected as visible and that all default columns are in the list. + """ + columns = data["visible"] + data["hidden"] + request_columns_ids = [column["id"] for column in columns] + if len(data["visible"]) == 0: + raise ValidationError("At least one column should be selected as visible") + elif not set(request_columns_ids) >= set(AlertGroupTableDefaultColumnChoices.values): + raise ValidationError("Default column cannot be removed") + elif len(request_columns_ids) > len(set(request_columns_ids)): + raise ValidationError("Duplicate column") + return data + + +class AlertGroupTableColumnsUserSerializer(AlertGroupTableColumnsOrganizationSerializer): + def validate(self, data): + """ + Validate that all columns exist in organization alert group table columns list. + """ + data = super().validate(data) + columns = data["visible"] + data["hidden"] + request_columns_ids = [column["id"] for column in columns] + organization_columns = self.context["request"].auth.organization.alert_group_table_columns or default_columns() + organization_columns_ids = [column["id"] for column in organization_columns] + if set(organization_columns_ids) != set(request_columns_ids): + raise ValidationError("Invalid settings") + return data diff --git a/engine/apps/api/tests/test_alert_group_table_settings.py b/engine/apps/api/tests/test_alert_group_table_settings.py new file mode 100644 index 00000000..175f3308 --- /dev/null +++ b/engine/apps/api/tests/test_alert_group_table_settings.py @@ -0,0 +1,339 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + +from apps.api.alert_group_table_columns import alert_group_table_user_settings +from apps.api.permissions import LegacyAccessControlRole +from apps.user_management.constants import AlertGroupTableColumnTypeChoices, default_columns + +DEFAULT_COLUMNS = default_columns() + + +def columns_settings(add_column=None): + default_settings = {"visible": DEFAULT_COLUMNS[:], "hidden": [], "default": True} + if add_column: + default_settings["hidden"].append(add_column) + return default_settings + + +@pytest.mark.django_db +def test_get_columns( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token() + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + expected_result = alert_group_table_user_settings(user) + + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_result + + +@pytest.mark.parametrize( + "initial_columns_settings,updated_columns_settings,status_code", + [ + # add column + ( + columns_settings(), + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}), + status.HTTP_200_OK, + ), + # remove column + ( + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}), + columns_settings(), + status.HTTP_200_OK, + ), + # wrong data format + (columns_settings(), {}, status.HTTP_400_BAD_REQUEST), + (columns_settings(), {"visible": []}, status.HTTP_400_BAD_REQUEST), + (columns_settings(), {"hidden": []}, status.HTTP_400_BAD_REQUEST), + # wrong id + ( + columns_settings(), + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.DEFAULT.value}), + status.HTTP_400_BAD_REQUEST, + ), + # duplicate id + ( + columns_settings(), + columns_settings({"name": "Test", "id": 1, "type": AlertGroupTableColumnTypeChoices.DEFAULT.value}), + status.HTTP_400_BAD_REQUEST, + ), + # remove default column + ( + columns_settings(), + {"visible": DEFAULT_COLUMNS[:-1], "hidden": []}, + status.HTTP_400_BAD_REQUEST, + ), + ], +) +@pytest.mark.django_db +def test_update_columns_list( + initial_columns_settings, + updated_columns_settings, + status_code, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + """Test alert group table settings for organization (POST request)""" + organization, user, token = make_organization_and_user_with_plugin_token() + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + client.post(url, data=initial_columns_settings, format="json", **make_user_auth_headers(user, token)) + response = client.post(url, data=updated_columns_settings, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + assert response.json() == updated_columns_settings + + +@pytest.mark.parametrize( + "initial_columns_settings,updated_columns_settings,status_code", + [ + # hide column + (columns_settings(), {"visible": DEFAULT_COLUMNS[:-1], "hidden": DEFAULT_COLUMNS[-1:]}, status.HTTP_200_OK), + # make column visible + ({"visible": DEFAULT_COLUMNS[:-1], "hidden": DEFAULT_COLUMNS[-1:]}, columns_settings(), status.HTTP_200_OK), + # wrong data format + (columns_settings(), {}, status.HTTP_400_BAD_REQUEST), + (columns_settings(), {"visible": []}, status.HTTP_400_BAD_REQUEST), + (columns_settings(), {"hidden": []}, status.HTTP_400_BAD_REQUEST), + # hide all columns + (columns_settings(), {"visible": [], "hidden": DEFAULT_COLUMNS[:]}, status.HTTP_400_BAD_REQUEST), + # add column + ( + columns_settings(), + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}), + status.HTTP_400_BAD_REQUEST, + ), + # remove column + ( + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}), + columns_settings(), + status.HTTP_400_BAD_REQUEST, + ), + ], +) +@pytest.mark.django_db +def test_update_columns_settings( + initial_columns_settings, + updated_columns_settings, + status_code, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + """Test alert group table settings for user (PUT request)""" + organization, user, token = make_organization_and_user_with_plugin_token() + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + client.post(url, data=initial_columns_settings, format="json", **make_user_auth_headers(user, token)) + response = client.put(url, data=updated_columns_settings, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + updated_columns_settings["default"] = updated_columns_settings["visible"] == DEFAULT_COLUMNS + assert response.json() == updated_columns_settings + + +@pytest.mark.django_db +def test_reset_user_columns( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + """Test reset alert group table settings for user""" + organization, user, token = make_organization_and_user_with_plugin_token() + client = APIClient() + url = reverse("api-internal:alert_group_table-reset_columns_settings") + new_column = {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value} + organization.update_alert_group_table_columns(default_columns() + [new_column]) + user.update_alert_group_table_selected_columns(organization.alert_group_table_columns[1::-1]) + default_settings = columns_settings(new_column) + assert alert_group_table_user_settings(user) != default_settings + response = client.post(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert response.json() == default_settings + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), + ], +) +def test_get_columns_permissions( + role, + expected_status, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), + ], +) +def test_update_columns_list_permissions( + role, + expected_status, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + data = columns_settings() + response = client.post(url, data=data, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), + ], +) +def test_update_columns_settings_permissions( + role, + expected_status, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + data = columns_settings() + response = client.put(url, data=data, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), + ], +) +def test_reset_user_columns_permissions( + role, + expected_status, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + url = reverse("api-internal:alert_group_table-reset_columns_settings") + response = client.post(url, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == expected_status + + +@pytest.mark.parametrize( + "user_settings,organization_settings,expected_result", + [ + # user doesn't have settings, organization has default settings - all columns are visible + ( + None, + DEFAULT_COLUMNS, + {"visible": DEFAULT_COLUMNS, "hidden": [], "default": True}, + ), + # user doesn't have settings, organization has updated settings - only default columns are visible + ( + None, + DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + { + "visible": DEFAULT_COLUMNS, + "hidden": [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + "default": True, + }, + ), + # user has settings, organization has default settings - only selected columns are visible + ( + DEFAULT_COLUMNS[:3], + DEFAULT_COLUMNS, + {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:], "default": False}, + ), + # user has settings, organization has unchanged settings - only selected columns are visible + ( + DEFAULT_COLUMNS[:3] + + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + { + "visible": ( + DEFAULT_COLUMNS[:3] + + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}] + ), + "hidden": DEFAULT_COLUMNS[3:], + "default": False, + }, + ), + # user has settings, organization has updated settings - column was removed, remove from settings + ( + DEFAULT_COLUMNS[:3] + + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + DEFAULT_COLUMNS, + {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:], "default": False}, + ), + # user has settings with reordered columns, organization has unchanged settings - selected columns in particular + # order are visible + ( + [ + DEFAULT_COLUMNS[1], + DEFAULT_COLUMNS[3], + {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}, + DEFAULT_COLUMNS[2], + ], + DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + { + "visible": [ + DEFAULT_COLUMNS[1], + DEFAULT_COLUMNS[3], + {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}, + DEFAULT_COLUMNS[2], + ], + "hidden": DEFAULT_COLUMNS[:1] + DEFAULT_COLUMNS[4:], + "default": False, + }, + ), + ], +) +@pytest.mark.django_db +def test_alert_group_table_user_settings( + user_settings, + organization_settings, + expected_result, + make_organization_and_user, +): + organization, user = make_organization_and_user() + organization.update_alert_group_table_columns(organization_settings) + if user_settings: + user.update_alert_group_table_selected_columns(user_settings) + result = alert_group_table_user_settings(user) + assert result == expected_result + assert user.alert_group_table_selected_columns == result["visible"] diff --git a/engine/apps/api/urls.py b/engine/apps/api/urls.py index c8c79a1e..80137dc0 100644 --- a/engine/apps/api/urls.py +++ b/engine/apps/api/urls.py @@ -4,6 +4,7 @@ from common.api_helpers.optional_slash_router import OptionalSlashRouter, option from .views import UserNotificationPolicyView, auth from .views.alert_group import AlertGroupView +from .views.alert_group_table_settings import AlertGroupTableColumnsViewSet from .views.alert_receive_channel import AlertReceiveChannelView from .views.alert_receive_channel_template import AlertReceiveChannelTemplateView from .views.alerts import AlertDetailView @@ -143,3 +144,19 @@ urlpatterns += [ name="alert_group_labels-get_key", ), ] + +# Alert group table settings +urlpatterns += [ + re_path( + r"^alertgroup_table_settings/?$", + AlertGroupTableColumnsViewSet.as_view( + {"get": "get_columns", "put": "update_user_columns", "post": "update_organization_columns"} + ), + name="alert_group_table-columns_settings", + ), + re_path( + r"^alertgroup_table_settings/reset?$", + AlertGroupTableColumnsViewSet.as_view({"post": "reset_user_columns"}), + name="alert_group_table-reset_columns_settings", + ), +] diff --git a/engine/apps/api/views/alert_group_table_settings.py b/engine/apps/api/views/alert_group_table_settings.py new file mode 100644 index 00000000..3b0a35f4 --- /dev/null +++ b/engine/apps/api/views/alert_group_table_settings.py @@ -0,0 +1,55 @@ +import typing + +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response + +from apps.api.alert_group_table_columns import alert_group_table_user_settings +from apps.api.permissions import RBACPermission +from apps.api.serializers.alert_group_table_settings import ( + AlertGroupTableColumnsOrganizationSerializer, + AlertGroupTableColumnsUserSerializer, +) +from apps.api.views.labels import LabelsFeatureFlagViewSet +from apps.auth_token.auth import PluginAuthentication +from apps.user_management.constants import AlertGroupTableColumn, default_columns + + +class AlertGroupTableColumnsViewSet(LabelsFeatureFlagViewSet): + authentication_classes = (PluginAuthentication,) + permission_classes = (IsAuthenticated, RBACPermission) + + rbac_permissions = { + "get_columns": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "update_user_columns": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "reset_user_columns": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "update_organization_columns": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + } + + def get_columns(self, request: Request) -> Response: + return Response(alert_group_table_user_settings(request.user)) + + def update_organization_columns(self, request: Request) -> Response: + """add/remove columns for organization""" + serializer = AlertGroupTableColumnsOrganizationSerializer(data=request.data, context={"request": request}) + serializer.is_valid(raise_exception=True) + columns: typing.List[AlertGroupTableColumn] = serializer.validated_data.get( + "visible", [] + ) + serializer.validated_data.get("hidden", []) + request.auth.organization.update_alert_group_table_columns(columns) + return Response(alert_group_table_user_settings(request.user)) + + def update_user_columns(self, request: Request) -> Response: + """select/hide/change order for user""" + user = request.user + serializer = AlertGroupTableColumnsUserSerializer(data=request.data, context={"request": request}) + serializer.is_valid(raise_exception=True) + columns: typing.List[AlertGroupTableColumn] = serializer.validated_data.get("visible", []) + user.update_alert_group_table_selected_columns(columns) + return Response(alert_group_table_user_settings(user)) + + def reset_user_columns(self, request: Request) -> Response: + """set default alert group table settings for user""" + user = request.user + user.update_alert_group_table_selected_columns(default_columns()) + return Response(alert_group_table_user_settings(user)) diff --git a/engine/apps/user_management/constants.py b/engine/apps/user_management/constants.py new file mode 100644 index 00000000..16c3b4ff --- /dev/null +++ b/engine/apps/user_management/constants.py @@ -0,0 +1,39 @@ +import typing + +from django.db.models import TextChoices + + +class AlertGroupTableDefaultColumnChoices(TextChoices): + STATUS = "status", "Status" + ID = "id", "ID" + TITLE = "title", "Title" + ALERTS = "alerts", "Alerts" + INTEGRATION = "integration", "Integration" + CREATED = "created", "Created" + LABELS = "labels", "Labels" + TEAM = "team", "Team" + USERS = "users", "Users" + + +class AlertGroupTableColumnTypeChoices(TextChoices): + DEFAULT = "default" + LABEL = "label" + + +class AlertGroupTableColumn(typing.TypedDict): + id: str + name: str + type: str + + +class AlertGroupTableColumns(typing.TypedDict): + visible: typing.List[AlertGroupTableColumn] + hidden: typing.List[AlertGroupTableColumn] + default: bool + + +def default_columns() -> typing.List[AlertGroupTableColumn]: + return [ + {"name": column.label, "id": column.value, "type": AlertGroupTableColumnTypeChoices.DEFAULT.value} + for column in AlertGroupTableDefaultColumnChoices + ] diff --git a/engine/apps/user_management/migrations/0018_auto_20231115_1206.py b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py new file mode 100644 index 00000000..5e92d9ad --- /dev/null +++ b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.20 on 2023-11-15 12:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_management', '0017_alter_organization_maintenance_author'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='alert_group_table_columns', + field=models.JSONField(default=None, null=True), + ), + migrations.AddField( + model_name='user', + name='alert_group_table_selected_columns', + field=models.JSONField(default=None, null=True), + ), + ] diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index 70fcf8e0..d3a16bf2 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -6,11 +6,12 @@ from urllib.parse import urljoin from django.conf import settings from django.core.validators import MinLengthValidator from django.db import models -from django.db.models import Count, Q +from django.db.models import Count, JSONField, Q from django.utils import timezone from mirage import fields as mirage_fields from apps.alerts.models import MaintainableObject +from apps.user_management.constants import AlertGroupTableColumn from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log from common.oncall_gateway import create_oncall_connector, delete_oncall_connector, delete_slack_connector @@ -248,6 +249,8 @@ class Organization(MaintainableObject): is_rbac_permissions_enabled = models.BooleanField(default=False) is_grafana_incident_enabled = models.BooleanField(default=False) + alert_group_table_columns: list[AlertGroupTableColumn] | None = JSONField(default=None, null=True) + class Meta: unique_together = ("stack_id", "org_id") @@ -283,6 +286,11 @@ class Organization(MaintainableObject): def emails_left(self, user): return self.subscription_strategy.emails_left(user) + def update_alert_group_table_columns(self, columns: typing.List[AlertGroupTableColumn]) -> None: + if columns != self.alert_group_table_columns: + self.alert_group_table_columns = columns + self.save(update_fields=["alert_group_table_columns"]) + def set_general_log_channel(self, channel_id, channel_name, user): if self.general_log_channel_id != channel_id: old_general_log_channel_id = self.slack_team_identity.cached_channels.filter( diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index d6dbbd71..42a942b5 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -21,6 +21,7 @@ from apps.api.permissions import ( user_is_authorized, ) from apps.schedules.tasks import drop_cached_ical_for_custom_events_for_organization +from apps.user_management.constants import AlertGroupTableColumn from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length if typing.TYPE_CHECKING: @@ -236,6 +237,8 @@ class User(models.Model): is_active = models.BooleanField(null=True, default=True) permissions = models.JSONField(null=False, default=list) + alert_group_table_selected_columns: list[AlertGroupTableColumn] | None = models.JSONField(default=None, null=True) + def __str__(self): return f"{self.pk}: {self.username}" @@ -449,6 +452,11 @@ class User(models.Model): ), ) + def update_alert_group_table_selected_columns(self, columns: typing.List[AlertGroupTableColumn]) -> None: + if self.alert_group_table_selected_columns != columns: + self.alert_group_table_selected_columns = columns + self.save(update_fields=["alert_group_table_selected_columns"]) + # TODO: check whether this signal can be moved to save method of the model @receiver(post_save, sender=User) diff --git a/grafana-plugin/e2e-tests/utils/alertGroup.ts b/grafana-plugin/e2e-tests/utils/alertGroup.ts index 1a7d854d..33ef2462 100644 --- a/grafana-plugin/e2e-tests/utils/alertGroup.ts +++ b/grafana-plugin/e2e-tests/utils/alertGroup.ts @@ -55,20 +55,17 @@ export const filterAlertGroupsTableByIntegrationAndGoToDetailPage = async ( await selectElement.type(integrationName); await selectValuePickerValue(page, integrationName, false); - /** - * wait for the alert groups to be filtered then by this particular integration (toBeVisible assertion), - * then click on the alert group and go to the individual alert group page - */ - const firstTableRow = page.locator('table > tbody > tr:first-child'); - try { /** - * wait for up to 5 seconds for the alert groups to be filtered, if the first row does not correspond + * wait for up to 2 seconds for the alert groups to be filtered, if the first row does not correspond * to `integrationName` assume that the background workers have not created it yet and lets * recursively retry this function */ - await firstTableRow.getByText(integrationName).waitFor({ state: 'visible', timeout: 5000 }); - await firstTableRow.locator('td:nth-child(4) a').click(); + + await page.waitForTimeout(2000); + + expect(await page.locator('table > tbody > tr [data-testid=integration-name]').textContent()).toBe(integrationName); + await page.locator('table > tbody > tr [data-testid=integration-url]').click(); } catch (err) { return filterAlertGroupsTableByIntegrationAndGoToDetailPage(page, integrationName, (retryNum += 1)); } diff --git a/grafana-plugin/jest.setup.ts b/grafana-plugin/jest.setup.ts index 0dc74191..4ae785da 100644 --- a/grafana-plugin/jest.setup.ts +++ b/grafana-plugin/jest.setup.ts @@ -19,3 +19,34 @@ Object.defineProperty(window, 'matchMedia', { dispatchEvent: jest.fn(), })), }); + +Object.defineProperty(window, 'ResizeObserver', { + writable: true, + value: class ResizeObserver { + constructor(callback: ResizeObserverCallback) { + setTimeout(() => { + callback( + [ + { + contentRect: { + x: 1, + y: 2, + width: 500, + height: 500, + top: 100, + bottom: 0, + left: 100, + right: 0, + }, + target: {}, + } as ResizeObserverEntry, + ], + this + ); + }); + } + observe() {} + disconnect() {} + unobserve() {} + }, +}); diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 9a328a97..5219ad4a 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -79,7 +79,7 @@ "@types/react-dom": "^18.0.6", "@types/react-responsive": "^8.0.5", "@types/react-router-dom": "^5.3.3", - "@types/react-test-renderer": "^17.0.2", + "@types/react-test-renderer": "^18.0.5", "@types/react-transition-group": "^4.4.5", "@types/throttle-debounce": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.40.1", @@ -101,9 +101,9 @@ "openapi-typescript": "^7.0.0-next.4", "plop": "^2.7.4", "postcss-loader": "^7.0.1", - "react": "17.0.2", - "react-dom": "17.0.2", - "react-test-renderer": "^17.0.2", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-test-renderer": "^18.0.2", "stylelint-config-prettier": "^9.0.3", "stylelint-prettier": "^2.0.0", "ts-jest": "29.0.3", @@ -116,18 +116,22 @@ "node": ">=14" }, "dependencies": { + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@grafana/data": "^9.2.4", "@grafana/faro-web-sdk": "^1.0.0-beta4", "@grafana/faro-web-tracing": "^1.0.0-beta4", - "@grafana/labels": "1.3.4", + "@grafana/labels": "~1.3.5", "@grafana/runtime": "9.3.0-beta1", - "@grafana/ui": "^9.4.7", + "@grafana/ui": "^10.2.0", "@opentelemetry/api": "^1.3.0", "array-move": "^4.0.0", "change-case": "^4.1.1", "circular-dependency-plugin": "^5.2.2", "dayjs": "^1.11.5", "eslint-plugin-import": "^2.25.4", + "immutability-helper": "^3.1.1", "mobx": "5.13.0", "mobx-react": "6.1.1", "object-hash": "^3.0.0", diff --git a/grafana-plugin/src/assets/style/global.css b/grafana-plugin/src/assets/style/global.css index ef7ae849..5cc2b9ab 100644 --- a/grafana-plugin/src/assets/style/global.css +++ b/grafana-plugin/src/assets/style/global.css @@ -44,9 +44,8 @@ margin-bottom: 16px; } -.rc-table-cell { - padding-left: 4px; - padding-right: 4px; +td.rc-table-cell { + height: 44px !important; /* works better than break-all, especially for table headers */ word-break: break-word; diff --git a/grafana-plugin/src/assets/style/utils.css b/grafana-plugin/src/assets/style/utils.css index bbcc0df9..7b02d032 100644 --- a/grafana-plugin/src/assets/style/utils.css +++ b/grafana-plugin/src/assets/style/utils.css @@ -143,6 +143,10 @@ -webkit-box-orient: vertical; } +.overflow-child--line-1 { + -webkit-line-clamp: 1; +} + .break-word { word-break: break-all; } @@ -150,3 +154,40 @@ .line-clamp-3 { -webkit-line-clamp: 3; } + +/* ----- + * CSSTransitionGroup fading + */ + +.fade-enter { + max-height: 0; + opacity: 0; +} + +.fade-enter.fade-enter-active { + max-height: 50px; + opacity: 1; + transition: opacity 300ms ease-in, max-height 300ms ease-in; +} + +.fade-leave { + opacity: 1; + max-height: 50px; +} + +.fade-leave.fade-leave-active { + max-height: 0; + opacity: 0; + transition: opacity 300ms ease-in, max-height 300ms ease-in; +} + +.fade-exit { + opacity: 1; + max-height: 50px; +} + +.fade-exit.fade-exit-active { + max-height: 0; + opacity: 0; + transition: opacity 300ms ease-in, max-height 300ms ease-in; +} diff --git a/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx b/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx index 85329a4f..7f0a21c9 100644 --- a/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx +++ b/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx @@ -27,7 +27,7 @@ const CheatSheet = (props: CheatSheetProps) => { {cheatSheetName} cheatsheet - + {cheatSheetData.description}
@@ -70,7 +70,7 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => { {item.codeExample} openNotification('Example copied')}> - + diff --git a/grafana-plugin/src/components/GTable/GTable.tsx b/grafana-plugin/src/components/GTable/GTable.tsx index ac6283f4..a646c16b 100644 --- a/grafana-plugin/src/components/GTable/GTable.tsx +++ b/grafana-plugin/src/components/GTable/GTable.tsx @@ -115,7 +115,7 @@ const GTable = (props: Props
{item.canHoverIcon ? ( - + ) : ( )} diff --git a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx index 7454403a..c270e278 100644 --- a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx +++ b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx @@ -256,6 +256,7 @@ const IntegrationContactPoint: React.FC<{ return ( { window.open( @@ -277,6 +278,7 @@ const IntegrationContactPoint: React.FC<{ } > { alertReceiveChannelStore diff --git a/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx index b255fa60..8047ad54 100644 --- a/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx +++ b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx @@ -35,13 +35,13 @@ const IntegrationInputField: React.FC = ({
- {showEye && } + {showEye && } {showCopy && ( - + )} - {showExternal && } + {showExternal && }
diff --git a/grafana-plugin/src/components/Policy/NotificationPolicy.tsx b/grafana-plugin/src/components/Policy/NotificationPolicy.tsx index fd90261b..7f541056 100644 --- a/grafana-plugin/src/components/Policy/NotificationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/NotificationPolicy.tsx @@ -72,6 +72,7 @@ export class NotificationPolicy extends React.Component = ({ qualit Calculation methodology - + {expanded && ( diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.tsx b/grafana-plugin/src/components/SourceCode/SourceCode.tsx index ecc27c61..90f98dfa 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.tsx +++ b/grafana-plugin/src/components/SourceCode/SourceCode.tsx @@ -33,7 +33,13 @@ const SourceCode: FC = (props) => { > {showClipboardIconOnly ? ( - + ) : (
  • - Choose + Select...
    - - - -
    + class="css-1j2891d-Icon" + />
    @@ -493,18 +411,18 @@ exports[`AddResponders should render selected team and users properly 1`] = `
  • - Choose + Select...
    - - - -
    + class="css-1j2891d-Icon" + />
    @@ -630,18 +525,18 @@ exports[`AddResponders should render selected team and users properly 1`] = `
  • - Choose + Select...
    - - - -
    + class="css-1j2891d-Icon" + />
    @@ -767,81 +639,63 @@ exports[`AddResponders should render selected team and users properly 1`] = `
  • - - - +
    -
    -
    - - -
    - Learn more -
    -
    - - - + Learn more +
    +
    +
    -
    - -
    - about Default vs Important user personal notification settings + + + about Default vs Important user personal notification settings +
    diff --git a/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/__snapshots__/AddRespondersPopup.test.tsx.snap b/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/__snapshots__/AddRespondersPopup.test.tsx.snap index ba89c566..d9a80841 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/__snapshots__/AddRespondersPopup.test.tsx.snap +++ b/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/__snapshots__/AddRespondersPopup.test.tsx.snap @@ -7,81 +7,79 @@ exports[`AddRespondersPopup it shows a loading message initially 1`] = ` data-testid="add-responders-popup" >
    - - - -
    + class="css-1j2891d-Icon" + />
    - - - -
    +
    - Users - - + + +
    Loading...
    diff --git a/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/__snapshots__/NotificationPoliciesSelect.test.tsx.snap b/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/__snapshots__/NotificationPoliciesSelect.test.tsx.snap index 1308038d..a991d610 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/__snapshots__/NotificationPoliciesSelect.test.tsx.snap +++ b/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/__snapshots__/NotificationPoliciesSelect.test.tsx.snap @@ -3,7 +3,7 @@ exports[`NotificationPoliciesSelect disabled state 1`] = `
    Default
    @@ -41,23 +41,11 @@ exports[`NotificationPoliciesSelect disabled state 1`] = ` />
    - - - -
    + class="css-1j2891d-Icon" + />
    @@ -67,7 +55,7 @@ exports[`NotificationPoliciesSelect disabled state 1`] = ` exports[`NotificationPoliciesSelect it renders properly 1`] = `
    Default
    @@ -104,23 +92,11 @@ exports[`NotificationPoliciesSelect it renders properly 1`] = ` />
    - - - -
    + class="css-1j2891d-Icon" + />
    diff --git a/grafana-plugin/src/containers/AddResponders/parts/TeamResponder/__snapshots__/TeamResponder.test.tsx.snap b/grafana-plugin/src/containers/AddResponders/parts/TeamResponder/__snapshots__/TeamResponder.test.tsx.snap index 420483ec..aa6662ce 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/TeamResponder/__snapshots__/TeamResponder.test.tsx.snap +++ b/grafana-plugin/src/containers/AddResponders/parts/TeamResponder/__snapshots__/TeamResponder.test.tsx.snap @@ -4,18 +4,18 @@ exports[`TeamResponder it renders data properly 1`] = `
  • diff --git a/grafana-plugin/src/containers/AddResponders/parts/UserResponder/__snapshots__/UserResponder.test.tsx.snap b/grafana-plugin/src/containers/AddResponders/parts/UserResponder/__snapshots__/UserResponder.test.tsx.snap index 2311afb8..7698f4d3 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/UserResponder/__snapshots__/UserResponder.test.tsx.snap +++ b/grafana-plugin/src/containers/AddResponders/parts/UserResponder/__snapshots__/UserResponder.test.tsx.snap @@ -4,18 +4,18 @@ exports[`UserResponder it renders data properly 1`] = `
  • Important
    @@ -86,51 +86,28 @@ exports[`UserResponder it renders data properly 1`] = ` />
    - - - -
    + class="css-1j2891d-Icon" + />
    diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts new file mode 100644 index 00000000..5d78b09a --- /dev/null +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts @@ -0,0 +1,93 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; + +export const getColumnsSelectorStyles = (_theme: GrafanaTheme2) => { + return { + columnsSelectorView: css` + min-width: 230px; + `, + + columnsVisibleSection: css` + margin-bottom: 16px; + `, + + columnsHeader: css` + display: block !important; + margin-bottom: 16px; + `, + columnsHeaderSmall: css` + display: block !important; + margin-bottom: 8px; + `, + + columnsHeaderSecondary: css` + display: block; + margin-bottom: 8px; + `, + + columnsHiddenSection: css` + margin-bottom: 20px; + max-height: 250px; + overflow-y: auto; + `, + columnsSelectorButtons: css` + display: flex; + justify-content: flex-end; + gap: 8px; + width: 100%; + `, + + columnItem: css` + gap: 12px; + display: flex; + padding-left: 25px; + + &:hover .columns-icon-trash { + display: block; + } + `, + + columnsCheckbox: css` + position: absolute; + top: 2px; + left: 0; + `, + + columnsIcon: css` + display: block; + margin-left: auto; + position: relative; + top: -2px; + + &::before { + top: 50%; + left: 50%; + border-radius: 50%; + transform: translate(-50%, -60%); + } + `, + + columnsIconTrash: css` + display: none; + `, + + columnRow: css` + position: relative; + margin-bottom: 6px; + height: 22px; + `, + + columnName: css` + text-wrap: nowrap; + max-width: 180px; + text-overflow: ellipsis; + display: block; + overflow: hidden; + `, + + labelIcon: css` + margin: 0; + padding: 0; + `, + }; +}; diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx new file mode 100644 index 00000000..a8e9673f --- /dev/null +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -0,0 +1,257 @@ +import React, { useRef } from 'react'; + +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, + useSortable, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { Button, Checkbox, Icon, IconButton, LoadingPlaceholder, Tooltip, useStyles2 } from '@grafana/ui'; +import { observer } from 'mobx-react'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; + +import RenderConditionally from 'components/RenderConditionally/RenderConditionally'; +import Text from 'components/Text/Text'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { AlertGroupColumn, AlertGroupColumnType } from 'models/alertgroup/alertgroup.types'; +import { ActionKey } from 'models/loader/action-keys'; +import { useStore } from 'state/useStore'; +import { openErrorNotification } from 'utils'; +import { UserActions } from 'utils/authorization'; +import { WrapAutoLoadingState } from 'utils/decorators'; + +import { getColumnsSelectorStyles } from './ColumnsSelector.styles'; + +const TRANSITION_MS = 500; + +interface ColumnRowProps { + column: AlertGroupColumn; + onItemChange: (id: number | string) => void; + onColumnRemoval: (column: AlertGroupColumn) => void; +} + +const ColumnRow: React.FC = ({ column, onItemChange, onColumnRemoval }) => { + const dnd = useSortable({ id: column.id }); + + const styles = useStyles2(getColumnsSelectorStyles); + + const { attributes, listeners, setNodeRef, transform, transition } = dnd; + const columnElRef = useRef(undefined); + + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
    +
    + {column.name} + + {column.type === AlertGroupColumnType.LABEL && ( + + + + )} + + + + + + + + onColumnRemoval(column)} + /> + + +
    + + onItemChange(column.id)} + /> +
    + ); +}; + +interface ColumnsSelectorProps { + onColumnAddModalOpen(): void; + onConfirmRemovalModalOpen(column: AlertGroupColumn): void; +} + +export const ColumnsSelector: React.FC = observer( + ({ onColumnAddModalOpen, onConfirmRemovalModalOpen }) => { + const { alertGroupStore, loaderStore } = useStore(); + + const styles = useStyles2(getColumnsSelectorStyles); + + const { columns, isDefaultColumnOrder } = alertGroupStore; + + const visibleColumns = columns.filter((col) => col.isVisible); + const hiddenColumns = columns + .filter((col) => !col.isVisible) + .sort((a, b) => a.id.toString().localeCompare(b.id.toString())); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const isResetLoading = loaderStore.isLoading(ActionKey.RESET_COLUMNS_FROM_ALERT_GROUP); + + return ( +
    + + Columns Settings + + +
    + + Visible ({visibleColumns.length}) + + + handleDragEnd(ev, true)} + > + + + {visibleColumns.map((column) => ( + + + + ))} + + + +
    + +
    + + Hidden ({hiddenColumns.length}) + + + handleDragEnd(ev, false)} + > + + + {hiddenColumns.map((column) => ( + + + + ))} + + + +
    + +
    + + + + +
    +
    + ); + + async function onReset() { + await alertGroupStore.resetTableSettings(); + await alertGroupStore.fetchTableSettings(); + } + + async function onItemChange(id: string | number) { + const checkedItems = alertGroupStore.columns.filter((col) => col.isVisible); + if (checkedItems.length === 1 && checkedItems[0].id === id) { + openErrorNotification('At least one column should be selected'); + return; + } + + alertGroupStore.columns = alertGroupStore.columns.map((item): AlertGroupColumn => { + let newItem: AlertGroupColumn = { ...item, isVisible: !item.isVisible }; + return item.id === id ? newItem : item; + }); + + await alertGroupStore.updateTableSettings(convertColumnsToTableSettings(alertGroupStore.columns), true); + } + + function handleDragEnd(event: DragEndEvent, isVisible: boolean) { + const { active, over } = event; + + let searchableList: AlertGroupColumn[] = isVisible ? visibleColumns : hiddenColumns; + + if (active.id !== over.id) { + const oldIndex = searchableList.findIndex((item) => item.id === active.id); + const newIndex = searchableList.findIndex((item) => item.id === over.id); + + searchableList = arrayMove(searchableList, oldIndex, newIndex); + + const updatedList = isVisible ? [...searchableList, ...hiddenColumns] : [...visibleColumns, ...searchableList]; + alertGroupStore.columns = updatedList; + } + } + } +); + +export function convertColumnsToTableSettings(columns: AlertGroupColumn[]): { + visible: AlertGroupColumn[]; + hidden: AlertGroupColumn[]; +} { + const tableSettings: { visible: AlertGroupColumn[]; hidden: AlertGroupColumn[] } = { + visible: columns.filter((col: AlertGroupColumn) => col.isVisible), + hidden: columns.filter((col: AlertGroupColumn) => !col.isVisible), + }; + + return tableSettings; +} diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx new file mode 100644 index 00000000..01df95ef --- /dev/null +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -0,0 +1,229 @@ +import React, { useMemo, useState } from 'react'; + +import { LabelTag } from '@grafana/labels'; +import { + Button, + Checkbox, + HorizontalGroup, + IconButton, + Input, + LoadingPlaceholder, + Modal, + VerticalGroup, + useStyles2, +} from '@grafana/ui'; +import { observer } from 'mobx-react'; + +import Block from 'components/GBlock/Block'; +import Text from 'components/Text/Text'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { AlertGroupColumn, AlertGroupColumnType } from 'models/alertgroup/alertgroup.types'; +import { ActionKey } from 'models/loader/action-keys'; +import { ApiSchemas } from 'network/oncall-api/api.types'; +import { components } from 'network/oncall-api/autogenerated-api.types'; +import { useStore } from 'state/useStore'; +import { openErrorNotification, pluralize } from 'utils'; +import { UserActions } from 'utils/authorization'; +import { useDebouncedCallback } from 'utils/hooks'; + +import { getColumnsSelectorWrapperStyles } from './ColumnsSelectorWrapper.styles'; + +interface ColumnsModalProps { + isModalOpen: boolean; + labelKeys: Array; + setIsModalOpen: (value: boolean) => void; + inputRef: React.RefObject; +} + +interface SearchResult extends Pick { + isChecked: boolean; + isCollapsed: boolean; + values: any[]; +} + +const DEBOUNCE_MS = 300; + +export const ColumnsModal: React.FC = observer( + ({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => { + const store = useStore(); + const styles = useStyles2(getColumnsSelectorWrapperStyles); + + const [searchResults, setSearchResults] = useState([]); + const debouncedOnInputChange = useDebouncedCallback(onInputChange, DEBOUNCE_MS); + + const isLoading = store.loaderStore.isLoading(ActionKey.ADD_NEW_COLUMN_TO_ALERT_GROUP); + + const availableKeysForSearching = useMemo(() => { + const currentAGColumns = store.alertGroupStore.columns.map((col) => col.name); + return labelKeys.filter((pair) => currentAGColumns.indexOf(pair.name) === -1); + }, [labelKeys, store.alertGroupStore.columns]); + + return ( + + +
    + + + + {inputRef?.current?.value === '' && ( + + {availableKeysForSearching.length} {pluralize('item', availableKeysForSearching.length)} available. + Type to see suggestions + + )} + + {inputRef?.current?.value && searchResults.length && ( + + {searchResults.map((result, index) => ( + +
    + expandOrCollapseSearchResultItem(result, index)} + /> + + { + setSearchResults((items) => { + return items.map((item) => { + const updatedItem: SearchResult = { ...item, isChecked: !item.isChecked }; + return item.id === result.id ? updatedItem : item; + }); + }); + }} + /> + + {result.name} +
    + {!result.isCollapsed && ( + + {result.values === undefined ? ( + + ) : ( + renderLabelValues(result.name, result.values) + )} + + )} +
    + ))} +
    + )} + + {inputRef?.current?.value && searchResults.length === 0 && ( + 0 results for your search. + )} +
    +
    + + + + + + + +
    +
    + ); + + function renderLabelValues(keyName: string, values: Array) { + return ( + + {values.slice(0, 2).map((val) => ( + + ))} +
    {values.length > 2 ? `+ ${values.length - 2}` : ``}
    +
    + ); + } + + async function expandOrCollapseSearchResultItem(result: SearchResult, index: number) { + setSearchResults((items) => + items.map((it, idx) => (idx === index ? { ...it, isCollapsed: !it.isCollapsed } : it)) + ); + + await fetchLabelValues(result, index); + } + + async function fetchLabelValues(result: SearchResult, index: number) { + const labelResponse = await store.alertGroupStore.loadValuesForLabelKey(result.id); + + setSearchResults((items) => + items.map((it, idx) => (idx === index ? { ...it, values: labelResponse.values } : it)) + ); + } + + function onCloseModal() { + inputRef.current.value = ''; + + setSearchResults([]); + setIsModalOpen(false); + setTimeout(forceOpenToggletip, 0); + } + + async function onAddNewColumns() { + const mergedColumns = [ + ...store.alertGroupStore.columns, + ...searchResults + .filter((item) => item.isChecked) + .map( + (item): AlertGroupColumn => ({ + id: item.id, + name: item.name, + isVisible: false, + type: AlertGroupColumnType.LABEL, + }) + ), + ]; + + const columns: { visible: AlertGroupColumn[]; hidden: AlertGroupColumn[] } = { + visible: mergedColumns.filter((col) => col.isVisible), + hidden: mergedColumns.filter((col) => !col.isVisible), + }; + + try { + await store.alertGroupStore.updateTableSettings(columns, false); + await store.alertGroupStore.fetchTableSettings(); + + setIsModalOpen(false); + setTimeout(() => forceOpenToggletip(), 0); + setSearchResults([]); + + inputRef.current.value = ''; + } catch (ex) { + openErrorNotification('An error has occurred. Please try again'); + } + } + + function onInputChange() { + const search = inputRef?.current?.value; + + setSearchResults( + availableKeysForSearching + .filter((pair) => pair.name.indexOf(search) > -1) + .map((pair) => ({ ...pair, isChecked: false, isCollapsed: true, values: undefined })) + ); + } + + function forceOpenToggletip() { + document.getElementById('toggletip-button')?.click(); + } + } +); diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts new file mode 100644 index 00000000..23a55dcd --- /dev/null +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts @@ -0,0 +1,49 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; + +export const getColumnsSelectorWrapperStyles = (theme: GrafanaTheme2) => { + return { + input: css` + margin-bottom: 16px; + `, + fieldRow: css` + width: 100%; + display: flex; + flex-direction: row; + align-items: 'center'; + gap: 16px; + `, + content: css` + width: 100%; + min-height: 100px; + `, + removalModal: css` + max-width: 500px; + `, + totalValuesCount: css` + margin-left: 16px; + `, + valuesBlock: css` + margin-bottom: 12px; + `, + floatingContainer: css` + position: relative; + `, + floatingContent: css` + position: absolute; + top: 40px; + right: 0; + display: none; + background-color: ${theme.colors.background.secondary}; + padding: 16px; + z-index: 101; + overflow: hidden; + `, + floatingContentVisible: css` + display: block; + `, + checkboxAddOption: css` + top: 3px; + `, + }; +}; diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx new file mode 100644 index 00000000..b4223bd2 --- /dev/null +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -0,0 +1,159 @@ +import React, { useEffect, useRef, useState } from 'react'; + +import { useStyles2, Button, HorizontalGroup, Icon, LoadingPlaceholder, Modal, VerticalGroup } from '@grafana/ui'; +import { observer } from 'mobx-react'; + +import Text from 'components/Text/Text'; +import { ColumnsSelector, convertColumnsToTableSettings } from 'containers/ColumnsSelector/ColumnsSelector'; +import { getColumnsSelectorWrapperStyles } from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { AlertGroupColumn } from 'models/alertgroup/alertgroup.types'; +import { ActionKey } from 'models/loader/action-keys'; +import { ApiSchemas } from 'network/oncall-api/api.types'; +import { useStore } from 'state/useStore'; +import { UserActions } from 'utils/authorization'; +import { WrapAutoLoadingState } from 'utils/decorators'; + +import { ColumnsModal } from './ColumnsModal'; + +interface ColumnsSelectorWrapperProps {} + +const ColumnsSelectorWrapper: React.FC = observer(() => { + const [isConfirmRemovalModalOpen, setIsConfirmRemovalModalOpen] = useState(false); + const [columnToBeRemoved, setColumnToBeRemoved] = useState(undefined); + const [isColumnAddModalOpen, setIsColumnAddModalOpen] = useState(false); + const [isFloatingDisplayOpen, setIsFloatingDisplayOpen] = useState(false); + + const [labelKeys, setLabelKeys] = useState>([]); + + const inputRef = useRef(null); + const wrappingFloatingContainerRef = useRef(null); + + const styles = useStyles2(getColumnsSelectorWrapperStyles); + + const store = useStore(); + + useEffect(() => { + isColumnAddModalOpen && + (async function () { + const keys = await store.alertGroupStore.loadLabelsKeys(); + setLabelKeys(keys); + })(); + }, [isColumnAddModalOpen]); + + useEffect(() => { + document.addEventListener('click', onFloatingDisplayClick); + + return () => { + document.removeEventListener('click', onFloatingDisplayClick); + }; + }, []); + + const isRemoveLoading = store.loaderStore.isLoading(ActionKey.REMOVE_COLUMN_FROM_ALERT_GROUP); + + return ( + <> + + + + + Are you sure you want to remove column {columnToBeRemoved?.name}? + + + + + + + + + + +
    + {!isColumnAddModalOpen && !isConfirmRemovalModalOpen ? ( +
    + {renderToggletipButton()} +
    + setIsColumnAddModalOpen(!isColumnAddModalOpen)} + onConfirmRemovalModalOpen={(column: AlertGroupColumn) => { + setIsConfirmRemovalModalOpen(!isConfirmRemovalModalOpen); + setColumnToBeRemoved(column); + }} + /> +
    +
    + ) : ( + renderToggletipButton() + )} +
    + + ); + + function onFloatingDisplayClick(event) { + const element = wrappingFloatingContainerRef.current; + const isInside = element?.contains(event.target as HTMLDivElement); + + if (!isInside) { + setIsFloatingDisplayOpen(false); + } + } + + function onConfirmRemovalClose(): void { + setIsConfirmRemovalModalOpen(false); + forceOpenToggletip(); + } + + async function onColumnRemovalClick(): Promise { + const columns = store.alertGroupStore.columns.filter((col) => col.id !== columnToBeRemoved.id); + + await store.alertGroupStore.updateTableSettings(convertColumnsToTableSettings(columns), false); + await store.alertGroupStore.fetchTableSettings(); + + setIsConfirmRemovalModalOpen(false); + forceOpenToggletip(); + } + + function renderToggletipButton() { + return ( + + ); + } +}); + +function forceOpenToggletip() { + document.getElementById('toggletip-button')?.click(); +} + +export default ColumnsSelectorWrapper; diff --git a/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx b/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx index 42fe0271..ed90f589 100644 --- a/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx +++ b/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx @@ -106,7 +106,12 @@ class IncidentsFilters extends Component (
    {capitalCase(filterOption.name)}: {this.renderFilterOption(filterOption)} - +
    ))} +
    + +
    diff --git a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx index 1b2127d7..33102a8f 100644 --- a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx +++ b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx @@ -143,7 +143,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { Edit custom payload - +
  • @@ -263,7 +263,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { Edit custom payload - returnToListView()} /> + returnToListView()} />
    @@ -292,8 +292,8 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { {selectedTitle}
    - setIsEditMode(true)} /> - returnToListView()} /> + setIsEditMode(true)} /> + returnToListView()} />
    diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 4e7069e3..93c29049 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -3,6 +3,7 @@ import qs from 'query-string'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import BaseStore from 'models/base_store'; +import { ActionKey } from 'models/loader/action-keys'; import { User } from 'models/user/user.types'; import { makeRequest } from 'network'; import { ApiSchemas } from 'network/oncall-api/api.types'; @@ -11,8 +12,9 @@ import { RootStore } from 'state'; import { SelectOption } from 'state/types'; import { openErrorNotification, refreshPageError, showApiError } from 'utils'; import LocationHelper from 'utils/LocationHelper'; +import { AutoLoadingState } from 'utils/decorators'; -import { Alert, AlertAction, IncidentStatus } from './alertgroup.types'; +import { AlertGroupColumn, Alert, AlertAction, IncidentStatus } from './alertgroup.types'; export class AlertGroupStore extends BaseStore { @observable.shallow @@ -69,6 +71,12 @@ export class AlertGroupStore extends BaseStore { @observable liveUpdatesPaused = false; + @observable + columns: AlertGroupColumn[] = []; + + @observable + isDefaultColumnOrder = false; + constructor(rootStore: RootStore) { super(rootStore); @@ -429,21 +437,64 @@ export class AlertGroupStore extends BaseStore { } @action - public async loadLabelsKeys() { - return await makeRequest(`/alertgroups/labels/keys/`, {}); + async fetchTableSettings(): Promise { + const tableSettings = await makeRequest('/alertgroup_table_settings', {}); + + const { hidden, visible, default: isDefaultOrder } = tableSettings; + + this.isDefaultColumnOrder = isDefaultOrder; + this.columns = [ + ...visible.map((item: AlertGroupColumn): AlertGroupColumn => ({ ...item, isVisible: true })), + ...hidden.map((item: AlertGroupColumn): AlertGroupColumn => ({ ...item, isVisible: false })), + ]; } @action - public async loadValuesForLabelKey(key: ApiSchemas['LabelKey']['id'], search = '') { + @AutoLoadingState(ActionKey.ADD_NEW_COLUMN_TO_ALERT_GROUP) + async updateTableSettings( + columns: { visible: AlertGroupColumn[]; hidden: AlertGroupColumn[] }, + isUserUpdate: boolean + ): Promise { + const method = isUserUpdate ? 'PUT' : 'POST'; + + const { default: isDefaultOrder } = await makeRequest('/alertgroup_table_settings', { + method, + data: { ...columns }, + }); + + this.isDefaultColumnOrder = isDefaultOrder; + } + + @action + async resetTableSettings(): Promise { + return await makeRequest('/alertgroup_table_settings/reset', { method: 'POST' }).catch(() => + openErrorNotification('There was an error resetting the table settings') + ); + } + + @action + async loadLabelsKeys(): Promise> { + return await makeRequest(`/alertgroups/labels/keys/`, {}).catch(() => + openErrorNotification('There was an error processing your request') + ); + } + + @action + async loadValuesForLabelKey( + key: ApiSchemas['LabelKey']['id'], + search = '' + ): Promise<{ key: ApiSchemas['LabelKey']; values: Array }> { if (!key) { - return []; + return { key: undefined, values: [] }; } const result = await makeRequest(`/alertgroups/labels/id/${key}`, { params: { search }, }); - const filteredValues = result.values.filter((v) => v.name.toLowerCase().includes(search.toLowerCase())); // TODO remove after backend search implementation + const filteredValues = result.values.filter((v: ApiSchemas['LabelValue']) => + v.name.toLowerCase().includes(search.toLowerCase()) + ); return { ...result, values: filteredValues }; } diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index 082c6186..72374249 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -90,6 +90,18 @@ export interface Alert { undoAction?: AlertAction; } +export interface AlertGroupColumn { + id: string; + name: string; + isVisible: boolean; + type?: AlertGroupColumnType; +} + +export enum AlertGroupColumnType { + DEFAULT = 'default', + LABEL = 'label', +} + interface RenderForWeb { message: any; title: any; diff --git a/grafana-plugin/src/models/loader/action-keys.ts b/grafana-plugin/src/models/loader/action-keys.ts new file mode 100644 index 00000000..3d5898d8 --- /dev/null +++ b/grafana-plugin/src/models/loader/action-keys.ts @@ -0,0 +1,5 @@ +export enum ActionKey { + ADD_NEW_COLUMN_TO_ALERT_GROUP = 'ADD_NEW_COLUMN_TO_ALERT_GROUP', + REMOVE_COLUMN_FROM_ALERT_GROUP = 'REMOVE_COLUMN_FROM_ALERT_GROUP', + RESET_COLUMNS_FROM_ALERT_GROUP = 'RESET_COLUMNS_FROM_ALERT_GROUP', +} diff --git a/grafana-plugin/src/models/loader/loader.ts b/grafana-plugin/src/models/loader/loader.ts new file mode 100644 index 00000000..4606bb45 --- /dev/null +++ b/grafana-plugin/src/models/loader/loader.ts @@ -0,0 +1,21 @@ +import { action, observable } from 'mobx'; + +interface LoadingResult { + [key: string]: boolean; +} + +class LoaderStoreClass { + @observable + items: LoadingResult = {}; + + @action + setLoadingAction(actionKey: string, isLoading: boolean) { + this.items[actionKey] = isLoading; + } + + isLoading(actionKey: string): boolean { + return !!this.items[actionKey]; + } +} + +export const LoaderStore = new LoaderStoreClass(); diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 872e4c16..6ddca9ae 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -177,7 +177,7 @@ class EscalationChainsPage extends React.Component ) : ( - + Loading... diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index d59b50cb..a0a715dc 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -283,7 +283,7 @@ class IncidentPage extends React.Component - + {/* @ts-ignore*/} @@ -765,7 +765,7 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle
    - openIncidentResponse(incident)} /> + openIncidentResponse(incident)} />
    diff --git a/grafana-plugin/src/pages/incidents/Incidents.module.scss b/grafana-plugin/src/pages/incidents/Incidents.module.scss index b540eee5..3281f8ff 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.module.scss +++ b/grafana-plugin/src/pages/incidents/Incidents.module.scss @@ -11,14 +11,29 @@ margin-bottom: 20px; } +.fields-dropdown { + gap: 8px; + display: flex; + margin-left: auto; + align-items: center; + padding-left: 4px; +} + .above-incidents-table { display: flex; justify-content: space-between; align-items: center; } -.bulk-actions { +.bulk-actions-container { margin: 10px 0 10px 0; + display: flex; + width: 100%; +} +.bulk-actions-list { + display: flex; + align-items: center; + gap: 8px; } .other-users { @@ -41,6 +56,10 @@ right: 0; } +.btn-results { + margin-left: 8px; +} + /* filter cards */ .cards { diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index f08abb31..30d47905 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -1,7 +1,9 @@ import React, { SyntheticEvent } from 'react'; -import { Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui'; +import { LabelTag } from '@grafana/labels'; +import { Button, HorizontalGroup, Icon, RadioButtonGroup, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; +import { capitalize } from 'lodash-es'; import { observer } from 'mobx-react'; import moment from 'moment-timezone'; import Emoji from 'react-emoji-render'; @@ -11,25 +13,37 @@ import CardButton from 'components/CardButton/CardButton'; import CursorPagination from 'components/CursorPagination/CursorPagination'; import GTable from 'components/GTable/GTable'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; -import LabelsTooltipBadge from 'components/LabelsTooltipBadge/LabelsTooltipBadge'; import ManualAlertGroup from 'components/ManualAlertGroup/ManualAlertGroup'; import PluginLink from 'components/PluginLink/PluginLink'; +import RenderConditionally from 'components/RenderConditionally/RenderConditionally'; import Text from 'components/Text/Text'; import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip'; +import TooltipBadge from 'components/TooltipBadge/TooltipBadge'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; +import ColumnsSelectorWrapper from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper'; import { IncidentsFiltersType } from 'containers/IncidentsFilters/IncidentFilters.types'; import RemoteFilters from 'containers/RemoteFilters/RemoteFilters'; import TeamName from 'containers/TeamName/TeamName'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { Alert, Alert as AlertType, AlertAction, IncidentStatus } from 'models/alertgroup/alertgroup.types'; +import { + Alert, + Alert as AlertType, + AlertAction, + IncidentStatus, + AlertGroupColumn, + AlertGroupColumnType, +} from 'models/alertgroup/alertgroup.types'; +import { LabelKeyValue } from 'models/label/label.types'; import { renderRelatedUsers } from 'pages/incident/Incident.helpers'; import { AppFeature } from 'state/features'; import { PageProps, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; import LocationHelper from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization'; -import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; +import { INCIDENT_HORIZONTAL_SCROLLING_STORAGE, PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; +import { getItem, setItem } from 'utils/localStorage'; +import { TableColumn } from 'utils/types'; import styles from './Incidents.module.scss'; import { IncidentDropdown } from './parts/IncidentDropdown'; @@ -49,6 +63,8 @@ interface IncidentsPageState { filters?: Record; pagination: Pagination; showAddAlertGroupForm: boolean; + isSelectorColumnMenuOpen: boolean; + isHorizontalScrolling: boolean; } const POLLING_NUM_SECONDS = 15; @@ -59,6 +75,14 @@ const PAGINATION_OPTIONS = [ { label: '100', value: 100 }, ]; +const TABLE_SCROLL_OPTIONS: Array<{ value: boolean; icon: string }> = [ + { value: false, icon: 'wrap-text' }, + { + value: true, + icon: 'arrow-from-right', + }, +]; + @observer class Incidents extends React.Component { constructor(props: IncidentsPageProps) { @@ -82,16 +106,23 @@ class Incidents extends React.Component start, end: start + pageSize, }, + isSelectorColumnMenuOpen: true, + isHorizontalScrolling: getItem(INCIDENT_HORIZONTAL_SCROLLING_STORAGE) || false, }; } private pollingIntervalId: NodeJS.Timer = undefined; componentDidMount() { - const { alertGroupStore } = this.props.store; + const { store } = this.props; + const { alertGroupStore } = store; alertGroupStore.updateBulkActions(); alertGroupStore.updateSilenceOptions(); + + if (store.hasFeature(AppFeature.Labels)) { + alertGroupStore.fetchTableSettings(); + } } componentWillUnmount(): void { @@ -335,6 +366,11 @@ class Incidents extends React.Component ); }; + onEnableHorizontalScroll = (value: boolean) => { + setItem(INCIDENT_HORIZONTAL_SCROLLING_STORAGE, value); + this.setState({ isHorizontalScrolling: value }); + }; + handleChangeItemsPerPage = (value: number) => { const { store } = this.props; @@ -357,7 +393,7 @@ class Incidents extends React.Component }; renderBulkActions = () => { - const { selectedIncidentIds, affectedRows } = this.state; + const { selectedIncidentIds, affectedRows, isHorizontalScrolling } = this.state; const { store } = this.props; if (!store.alertGroupStore.bulkActions) { @@ -373,8 +409,8 @@ class Incidents extends React.Component return (
    -
    - +
    +
    {'resolve' in store.alertGroupStore.bulkActions && (
    - {hasInvalidatedAlert && ( -
    - Results out of date -
    - )} + +
    + + + Results out of date + + + + + + + + + + + +
    +
    ); }; renderTable() { - const { selectedIncidentIds, pagination } = this.state; + const { selectedIncidentIds, pagination, isHorizontalScrolling } = this.state; const { alertGroupStore, filtersStore } = this.props.store; const { results, prev, next } = alertGroupStore.getAlertSearchResult('default'); @@ -468,6 +519,8 @@ class Incidents extends React.Component ); } + const tableColumns = this.getTableColumns(); + return (
    {this.renderBulkActions()} @@ -481,7 +534,9 @@ class Incidents extends React.Component }} rowKey="pk" data={results} - columns={this.getTableColumns()} + columns={tableColumns} + tableLayout="auto" + scroll={{ x: isHorizontalScrolling ? `${Math.max(2000, tableColumns.length * 250)}px` : true }} /> {this.shouldShowPagination() && (
    @@ -503,7 +558,7 @@ class Incidents extends React.Component renderId(record: AlertType) { return ( - + #{record.inside_organization_number} @@ -518,7 +573,7 @@ class Incidents extends React.Component return (
    - + content={record?.alert_receive_channel?.verbal_name || ''} > - + ); }; @@ -574,16 +633,60 @@ class Incidents extends React.Component ); }; - renderStartedAt(alert: AlertType) { + renderStartedAt = (alert: AlertType) => { const m = moment(alert.started_at); + const { isHorizontalScrolling } = this.state; + + const date = m.format('MMM DD, YYYY'); + const time = m.format('HH:mm'); + + if (isHorizontalScrolling) { + // display date as 1 line + return ( + + {date} {time} + + ); + } return ( - {m.format('MMM DD, YYYY')} - {m.format('HH:mm')} + {date} + {time} ); - } + }; + + renderLabels = (item: AlertType) => { + if (!item.labels.length) { + return null; + } + + return ( + + {item.labels.map((label) => ( + + +
    Configure Plugin @@ -124,33 +124,33 @@ exports[`PluginSetup there is an error message 1`] = ` class="configure-plugin" >
    Configure Plugin diff --git a/grafana-plugin/src/state/rootBaseStore/index.ts b/grafana-plugin/src/state/rootBaseStore/index.ts index a905f5f2..44cc0f91 100644 --- a/grafana-plugin/src/state/rootBaseStore/index.ts +++ b/grafana-plugin/src/state/rootBaseStore/index.ts @@ -19,6 +19,7 @@ import { GlobalSettingStore } from 'models/global_setting/global_setting'; import { GrafanaTeamStore } from 'models/grafana_team/grafana_team'; import { HeartbeatStore } from 'models/heartbeat/heartbeat'; import { LabelStore } from 'models/label/label'; +import { LoaderStore } from 'models/loader/loader'; import { OrganizationStore } from 'models/organization/organization'; import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook'; import { ResolutionNotesStore } from 'models/resolution_note/resolution_note'; @@ -105,6 +106,7 @@ export class RootBaseStore { globalSettingStore = new GlobalSettingStore(this); filtersStore = new FiltersStore(this); labelsStore = new LabelStore(this); + loaderStore = LoaderStore; // stores diff --git a/grafana-plugin/src/state/useStore.ts b/grafana-plugin/src/state/useStore.ts index 9440c5b1..ebcda3ba 100644 --- a/grafana-plugin/src/state/useStore.ts +++ b/grafana-plugin/src/state/useStore.ts @@ -6,6 +6,5 @@ import { RootStore } from './index'; export function useStore(): RootStore { const { store } = React.useContext(MobXProviderContext); - return store; } diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index 6195082d..1857c08a 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -54,3 +54,5 @@ export enum PAGE { } export const TEXT_ELLIPSIS_CLASS = 'overflow-child'; + +export const INCIDENT_HORIZONTAL_SCROLLING_STORAGE = 'isIncidentalTableHorizontalScrolling'; diff --git a/grafana-plugin/src/utils/decorators.ts b/grafana-plugin/src/utils/decorators.ts index 05199be8..21f0c78e 100644 --- a/grafana-plugin/src/utils/decorators.ts +++ b/grafana-plugin/src/utils/decorators.ts @@ -1,5 +1,32 @@ +import { LoaderStore } from 'models/loader/loader'; import { openErrorNotification, openNotification, openWarningNotification } from 'utils'; +export function AutoLoadingState(actionKey: string) { + return function (_target: object, _key: string, descriptor: PropertyDescriptor) { + const originalFunction = descriptor.value; + descriptor.value = async function (...args: any) { + LoaderStore.setLoadingAction(actionKey, true); + try { + await originalFunction.apply(this, args); + } finally { + LoaderStore.setLoadingAction(actionKey, false); + } + }; + }; +} + +export function WrapAutoLoadingState(callback: Function, actionKey: string): (...params: any[]) => Promise { + return async (...params) => { + LoaderStore.setLoadingAction(actionKey, true); + + try { + await callback(...params); + } finally { + LoaderStore.setLoadingAction(actionKey, false); + } + }; +} + type GlobalNotificationConfig = { success?: string; failure?: string; diff --git a/grafana-plugin/src/utils/index.ts b/grafana-plugin/src/utils/index.ts index d3e4cd5a..27e477ae 100644 --- a/grafana-plugin/src/utils/index.ts +++ b/grafana-plugin/src/utils/index.ts @@ -91,3 +91,7 @@ export function getPaths(obj?: any, parentKey?: string): string[] { } return concat(result, parentKey || []); } + +export function pluralize(word: string, count: number): string { + return count === 1 ? word : `${word}s`; +} diff --git a/grafana-plugin/src/utils/types.ts b/grafana-plugin/src/utils/types.ts new file mode 100644 index 00000000..20eabc4b --- /dev/null +++ b/grafana-plugin/src/utils/types.ts @@ -0,0 +1,8 @@ +import { RenderedCell } from 'rc-table/lib/interface'; + +export interface TableColumn { + width?: string | number; + title: string; + key: string; + render: (value: any, record: any, index: number) => React.ReactNode | RenderedCell; +} diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 75c6b94f..c2da9267 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -1382,6 +1382,37 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== +"@dnd-kit/accessibility@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz#3ccbefdfca595b0a23a5dc57d3de96bc6935641c" + integrity sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^6.0.8": + version "6.0.8" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.8.tgz#040ae13fea9787ee078e5f0361f3b49b07f3f005" + integrity sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA== + dependencies: + "@dnd-kit/accessibility" "^3.0.0" + "@dnd-kit/utilities" "^3.2.1" + tslib "^2.0.0" + +"@dnd-kit/sortable@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-7.0.2.tgz#791d550872457f3f3c843e00d159b640f982011c" + integrity sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA== + dependencies: + "@dnd-kit/utilities" "^3.2.0" + tslib "^2.0.0" + +"@dnd-kit/utilities@^3.2.0", "@dnd-kit/utilities@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.1.tgz#53f9e2016fd2506ec49e404c289392cfff30332a" + integrity sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA== + dependencies: + tslib "^2.0.0" + "@emotion/babel-plugin@^11.10.5": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c" @@ -1721,6 +1752,37 @@ uplot "1.6.24" xss "^1.0.14" +"@grafana/data@10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@grafana/data/-/data-10.2.0.tgz#116190f47f51f2eebd608bd5299d7ce278899bbe" + integrity sha512-MPUmkokQY7AWbJKVundp9AtTZdk4HqZHUCNvM1TFkTACUW9rVCi5fmmjwJQFLfTJ9JL2fkls8Z6S1l9Hd9ViTw== + dependencies: + "@braintree/sanitize-url" "6.0.2" + "@grafana/schema" "10.2.0" + "@types/d3-interpolate" "^3.0.0" + "@types/string-hash" "1.1.1" + d3-interpolate "3.0.1" + date-fns "2.30.0" + dompurify "^2.4.3" + eventemitter3 "5.0.1" + fast_array_intersect "1.1.0" + history "4.10.1" + lodash "4.17.21" + marked "5.1.1" + marked-mangle "1.1.0" + moment "2.29.4" + moment-timezone "0.5.43" + ol "7.4.0" + papaparse "5.4.1" + react-use "17.4.0" + regenerator-runtime "0.13.11" + rxjs "7.8.1" + string-hash "^1.1.3" + tinycolor2 "1.6.0" + tslib "2.6.0" + uplot "1.6.26" + xss "^1.0.14" + "@grafana/data@9.3.0-beta1": version "9.3.0-beta1" resolved "https://registry.yarnpkg.com/@grafana/data/-/data-9.3.0-beta1.tgz#0c1d8da18b8f9a5c7e77312a1b36daf394bfc596" @@ -1747,33 +1809,6 @@ uplot "1.6.22" xss "1.0.14" -"@grafana/data@9.4.7": - version "9.4.7" - resolved "https://registry.yarnpkg.com/@grafana/data/-/data-9.4.7.tgz#8b4c15a5b52ec13908c006baf87416354ee8251a" - integrity sha512-GnP91XSuTlRaT4crRh7OgC58rKsF/ANAZTFeHOYqVD7r47upTgnnnM46khSLhvA3MoKfNZflXOneaIjU4c5Hyw== - dependencies: - "@braintree/sanitize-url" "6.0.1" - "@grafana/schema" "9.4.7" - "@types/d3-interpolate" "^3.0.0" - d3-interpolate "3.0.1" - date-fns "2.29.3" - eventemitter3 "4.0.7" - fast_array_intersect "1.1.0" - history "4.10.1" - lodash "4.17.21" - marked "4.2.0" - moment "2.29.4" - moment-timezone "0.5.38" - ol "7.1.0" - papaparse "5.3.2" - react-use "17.4.0" - regenerator-runtime "0.13.10" - rxjs "7.5.7" - tinycolor2 "1.4.2" - tslib "2.4.1" - uplot "1.6.24" - xss "1.0.14" - "@grafana/data@9.5.2": version "9.5.2" resolved "https://registry.yarnpkg.com/@grafana/data/-/data-9.5.2.tgz#983042208a61c3a321499da7837fdd2ecbe7a04c" @@ -1837,6 +1872,15 @@ tslib "2.6.0" typescript "4.8.4" +"@grafana/e2e-selectors@10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-10.2.0.tgz#12110b75376cfeeeeb33d8bb57fca2b4119febb7" + integrity sha512-mrYz7xri7H7TiYpDXQHeMHKMDzx2a9kIM0OklXhN1ZsQQSeYrh0+87EizyWeL0T7/d0OorLR4nq8zxVyVni8Bg== + dependencies: + "@grafana/tsconfig" "^1.2.0-rc1" + tslib "2.6.0" + typescript "4.8.4" + "@grafana/e2e-selectors@9.3.0-beta1": version "9.3.0-beta1" resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-9.3.0-beta1.tgz#49ca6a4957763a8fee8560a5cd7f546a3f4853d3" @@ -1846,15 +1890,6 @@ tslib "2.4.1" typescript "4.8.4" -"@grafana/e2e-selectors@9.4.7": - version "9.4.7" - resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-9.4.7.tgz#7632bf927dc885ddeea0a865084badf93b2d777a" - integrity sha512-HvLgA9gccMC1uPx5Q+858yPjkfD5O0Kekm0p/ufQn+BA8dFbPpqVVd5cnu+/J3duKKHOsGBvZIShIOKNzkYw8g== - dependencies: - "@grafana/tsconfig" "^1.2.0-rc1" - tslib "2.4.1" - typescript "4.8.4" - "@grafana/e2e-selectors@9.5.2": version "9.5.2" resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-9.5.2.tgz#8d56e0c11d7dfb85e0b9a908397abb58cfbc2325" @@ -1916,6 +1951,16 @@ "@opentelemetry/otlp-transformer" "^0.41.2" murmurhash-js "^1.0.0" +"@grafana/faro-core@^1.2.1": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@grafana/faro-core/-/faro-core-1.2.3.tgz#03faae6ee93664cfda39dfd3059f42bbdb7aeae0" + integrity sha512-y2cfow8JLMrvSC4/Pd64/hByoAw5MWOPHmvpAueWNL0Cohj0XOGbKllQjTjnmCnDuLjAARS0pKAVdyVPjw9s6Q== + dependencies: + "@opentelemetry/api" "^1.4.1" + "@opentelemetry/api-metrics" "^0.33.0" + "@opentelemetry/otlp-transformer" "^0.41.2" + murmurhash-js "^1.0.0" + "@grafana/faro-web-sdk@1.0.0-beta2": version "1.0.0-beta2" resolved "https://registry.yarnpkg.com/@grafana/faro-web-sdk/-/faro-web-sdk-1.0.0-beta2.tgz#d096a350d6366a108428a205753c797802eb480d" @@ -1943,6 +1988,15 @@ ua-parser-js "^1.0.32" web-vitals "^3.1.1" +"@grafana/faro-web-sdk@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@grafana/faro-web-sdk/-/faro-web-sdk-1.2.1.tgz#4818884bba26f07ebe563084fc0e4eed4108ef8d" + integrity sha512-86Bk3IjVNdV/WufkdPJVUvjx7PYKjPV5n2Szpn+dOewZqEDd1lIqhyFYqVVM9kdjT+ARbSzY5BZvb+r0Kh8tuQ== + dependencies: + "@grafana/faro-core" "^1.2.1" + ua-parser-js "^1.0.32" + web-vitals "^3.1.1" + "@grafana/faro-web-sdk@^1.0.0-beta4": version "1.0.0-beta4" resolved "https://registry.yarnpkg.com/@grafana/faro-web-sdk/-/faro-web-sdk-1.0.0-beta4.tgz#de9ec9b1201b4f02e3746f31dc0e7a3f77df47b3" @@ -1973,10 +2027,10 @@ "@opentelemetry/sdk-trace-web" "^1.8.0" "@opentelemetry/semantic-conventions" "^1.8.0" -"@grafana/labels@1.3.4": - version "1.3.4" - resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.3.4.tgz#8d9cdd215a80a1da1045d402c037be85d7efd6f5" - integrity sha512-YYCuLGvtrMz7KkbMc6qoNJQr6drDLo6mMI27LcqsTDMHCNO3uJWpzC1Q2Y9MIwctIuTFYhbgfLvIunEegCx6PQ== +"@grafana/labels@~1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.3.5.tgz#c53b64e12a6360d7558dc9bc0fff8c6b31983acb" + integrity sha512-e79Ef/Bg5mGx0Mx6qGB65+6Z8HUHwXE4V8rjpI8EalWjARu6JlF27YBH28vbRX0kl1jepZHOi9EwYyck9y73PA== dependencies: "@emotion/css" "^11.11.2" "@grafana/ui" "^10.0.0" @@ -2008,6 +2062,13 @@ dependencies: tslib "2.6.0" +"@grafana/schema@10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@grafana/schema/-/schema-10.2.0.tgz#4b21aed47cd521484c899ef26737b5e4cc2ea323" + integrity sha512-IvjlezsOfIRjnsOwTJ1qu1GWbq9Rz3ofFi2Pd+1Brza6Gn951Hv/5MlLwqIuZJ+VnSVs35ZlNOl3sz9uSq2ibg== + dependencies: + tslib "2.6.0" + "@grafana/schema@9.2.4": version "9.2.4" resolved "https://registry.yarnpkg.com/@grafana/schema/-/schema-9.2.4.tgz#d96bfa80ef0f5e59d83002544d4570c19d79b934" @@ -2022,13 +2083,6 @@ dependencies: tslib "2.4.1" -"@grafana/schema@9.4.7": - version "9.4.7" - resolved "https://registry.yarnpkg.com/@grafana/schema/-/schema-9.4.7.tgz#bb918ec7f096e0b81d7ead921ac1addeb265dd0e" - integrity sha512-uTrg/XmMhfxXTSRskNRdUzDCK9XdwHHnNJkfUltzSF5v16bc9iE1u/NrkuEBxoLh6hji9Gd6pw7mS0K9o9/0ww== - dependencies: - tslib "2.4.1" - "@grafana/schema@9.5.2": version "9.5.2" resolved "https://registry.yarnpkg.com/@grafana/schema/-/schema-9.5.2.tgz#336587ceb9cb1391b3d07d9e0372a7812cb7b215" @@ -2344,74 +2398,76 @@ uplot "1.6.24" uuid "9.0.0" -"@grafana/ui@^9.4.7": - version "9.4.7" - resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-9.4.7.tgz#19ed1b36db85013070da118f4d87f13abb38567c" - integrity sha512-MnEXrGRh3t4LkShP/Q0bfzFooiE4xbDagQ/17/B1VIwMWECsYeSQsEYuA2p/9yjTpOiL2YfB72uyAThpGYpQew== +"@grafana/ui@^10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-10.2.0.tgz#99920ee490d52c014b28d61ca0d9d86294a15f41" + integrity sha512-RzvR053LVV8qYRrfFPMjzEeABwahVOeyQPXmU5vmYccPolQYXbc8wp149wjTf5xdUygqQunRADI6sAqgRpVrdA== dependencies: - "@emotion/css" "11.10.5" - "@emotion/react" "11.10.5" - "@grafana/data" "9.4.7" - "@grafana/e2e-selectors" "9.4.7" - "@grafana/schema" "9.4.7" - "@leeoniya/ufuzzy" "0.9.0" - "@monaco-editor/react" "4.4.6" - "@popperjs/core" "2.11.6" - "@react-aria/button" "3.6.1" - "@react-aria/dialog" "3.3.1" - "@react-aria/focus" "3.8.0" - "@react-aria/menu" "3.6.1" - "@react-aria/overlays" "3.10.1" - "@react-aria/utils" "3.13.1" - "@react-stately/menu" "3.4.1" - "@sentry/browser" "6.19.7" + "@emotion/css" "11.11.2" + "@emotion/react" "11.11.1" + "@grafana/data" "10.2.0" + "@grafana/e2e-selectors" "10.2.0" + "@grafana/faro-web-sdk" "1.2.1" + "@grafana/schema" "10.2.0" + "@leeoniya/ufuzzy" "1.0.8" + "@monaco-editor/react" "4.6.0" + "@popperjs/core" "2.11.8" + "@react-aria/button" "3.8.0" + "@react-aria/dialog" "3.5.3" + "@react-aria/focus" "3.13.0" + "@react-aria/menu" "3.10.0" + "@react-aria/overlays" "3.15.0" + "@react-aria/utils" "3.18.0" + "@react-stately/menu" "3.5.3" ansicolor "1.1.100" calculate-size "1.1.1" classnames "2.3.2" - core-js "3.27.1" - d3 "7.8.2" - date-fns "2.29.3" + core-js "3.33.0" + d3 "7.8.5" + date-fns "2.30.0" hoist-non-react-statics "3.3.2" i18next "^22.0.0" - immutable "4.2.2" + i18next-browser-languagedetector "^7.0.2" + immutable "4.3.1" is-hotkey "0.2.0" - jquery "3.6.1" + jquery "3.7.0" lodash "4.17.21" - memoize-one "6.0.0" + micro-memoize "^4.1.2" moment "2.29.4" monaco-editor "0.34.0" - ol "7.1.0" + ol "7.4.0" prismjs "1.29.0" - rc-cascader "3.8.0" - rc-drawer "6.1.2" - rc-slider "10.1.0" + rc-cascader "3.18.1" + rc-drawer "6.5.2" + rc-slider "10.3.1" rc-time-picker "^3.7.3" - rc-tooltip "5.3.1" + rc-tooltip "6.0.1" react-beautiful-dnd "13.1.1" - react-calendar "3.9.0" + react-calendar "4.3.0" react-colorful "5.6.1" react-custom-scrollbars-2 "4.5.0" react-dropzone "14.2.3" react-highlight-words "0.20.0" react-hook-form "7.5.3" react-i18next "^12.0.0" - react-inlinesvg "3.0.1" + react-inlinesvg "3.0.2" + react-loading-skeleton "3.3.1" react-popper "2.3.0" react-popper-tooltip "4.4.2" - react-router-dom "^5.2.0" - react-select "5.6.0" + react-router-dom "5.3.3" + react-select "5.7.4" react-select-event "^5.1.0" react-table "7.8.0" react-transition-group "4.4.5" react-use "17.4.0" - react-window "1.8.8" - rxjs "7.5.7" + react-window "1.8.9" + rxjs "7.8.1" slate "0.47.9" slate-plain-serializer "0.7.13" slate-react "0.22.10" - tinycolor2 "1.4.2" - tslib "2.4.1" - uplot "1.6.24" + tinycolor2 "1.6.0" + tslib "2.6.0" + uplot "1.6.26" uuid "9.0.0" "@humanwhocodes/config-array@^0.11.6": @@ -2797,11 +2853,6 @@ resolved "https://registry.yarnpkg.com/@leeoniya/ufuzzy/-/ufuzzy-0.8.0.tgz#2ccfc29453e168ce5866bf6dee89771db404a7f7" integrity sha512-EOc0fEsIqe6CDZxC14efhybnPcXyJi7VaZby40mWASZD0CI78ONoF+4+LGlcT58jsAIwEims5ARbRqo+BVHEAQ== -"@leeoniya/ufuzzy@0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@leeoniya/ufuzzy/-/ufuzzy-0.9.0.tgz#efb8f19f64ef6ff754fc49935c9ad53ab99712c1" - integrity sha512-p2zWsX0GwO1x723Yhb3KLAoSwp1geQvzRPHgIoOR/0qn8Ptpsb3b01+W47iAYR/NWo0pX36XQoTU0alVRykMAg== - "@leeoniya/ufuzzy@1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@leeoniya/ufuzzy/-/ufuzzy-1.0.6.tgz#cbafcff1529d9592b92bd735f1e8b18f23eda983" @@ -2848,7 +2899,7 @@ dependencies: state-local "^1.0.6" -"@monaco-editor/loader@^1.3.3": +"@monaco-editor/loader@^1.3.3", "@monaco-editor/loader@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558" integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg== @@ -2870,6 +2921,13 @@ dependencies: "@monaco-editor/loader" "^1.3.3" +"@monaco-editor/react@4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119" + integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw== + dependencies: + "@monaco-editor/loader" "^1.4.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3230,6 +3288,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== +"@popperjs/core@2.11.8": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + "@rc-component/portal@^1.0.0-6": version "1.1.1" resolved "https://registry.yarnpkg.com/@rc-component/portal/-/portal-1.1.1.tgz#1a30ffe51c240b54360cba8e8bfc5d1f559325c4" @@ -4539,12 +4602,12 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react-test-renderer@^17.0.2": - version "17.0.2" - resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.2.tgz#5f800a39b12ac8d2a2149e7e1885215bcf4edbbf" - integrity sha512-+F1KONQTBHDBBhbHuT2GNydeMpPuviduXIVJRB7Y4nma4NR5DrTJfMMZ+jbhEHbpwL+Uqhs1WXh4KHiyrtYTPg== +"@types/react-test-renderer@^18.0.5": + version "18.0.5" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.5.tgz#b67a6ff37acd93d1b971ec4c838f69d52e772db0" + integrity sha512-PsnmF4Hpi61PTRX+dTxkjgDdtZ09kFFgPXczoF+yBfOVxn7xBLPvKP1BUrSasYHmerj33rhoJuvpIMsJuyRqHw== dependencies: - "@types/react" "^17" + "@types/react" "*" "@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.5": version "4.4.5" @@ -6290,11 +6353,6 @@ core-js@3.26.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.26.0.tgz#a516db0ed0811be10eac5d94f3b8463d03faccfe" integrity sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw== -core-js@3.27.1: - version "3.27.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.1.tgz#23cc909b315a6bb4e418bf40a52758af2103ba46" - integrity sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww== - core-js@3.28.0: version "3.28.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.28.0.tgz#ed8b9e99c273879fdfff0edfc77ee709a5800e4a" @@ -6305,6 +6363,11 @@ core-js@3.31.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.31.0.tgz#4471dd33e366c79d8c0977ed2d940821719db344" integrity sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ== +core-js@3.33.0: + version "3.33.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40" + integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw== + core-js@^2.4.0: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" @@ -8012,6 +8075,11 @@ eventemitter3@5.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.0.tgz#084eb7f5b5388df1451e63f4c2aafd71b217ccb3" integrity sha512-riuVbElZZNXLeLEoprfNYoDSwTBRR44X3mnhdI1YcnENpWTCsTTVZ2zFuqQcpoyqPQIUXdiPEU0ECAq0KQRaHg== +eventemitter3@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -9085,16 +9153,16 @@ immer@^9.0.7: resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.16.tgz#8e7caab80118c2b54b37ad43e05758cdefad0198" integrity sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ== +immutability-helper@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-3.1.1.tgz#2b86b2286ed3b1241c9e23b7b21e0444f52f77b7" + integrity sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ== + immutable@4.1.0, immutable@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ== -immutable@4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.2.tgz#2da9ff4384a4330c36d4d1bc88e90f9e0b0ccd16" - integrity sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og== - immutable@4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.4.tgz#83260d50889526b4b531a5e293709a77f7c55a2a" @@ -9105,6 +9173,11 @@ immutable@4.3.0: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== +immutable@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.1.tgz#17988b356097ab0719e2f741d56f3ec6c317f9dc" + integrity sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A== + import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -10888,6 +10961,11 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +micro-memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.1.2.tgz#ce719c1ba1e41592f1cd91c64c5f41dcbf135f36" + integrity sha512-+HzcV2H+rbSJzApgkj0NdTakkC+bnyeiUxgT6/m7mjcz1CmM22KYFKp+EVj1sWe4UYcnriJr5uqHQD/gMHLD+g== + micromark@~2.11.0: version "2.11.4" resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a" @@ -11061,6 +11139,13 @@ moment-timezone@0.5.41: dependencies: moment "^2.29.4" +moment-timezone@0.5.43: + version "0.5.43" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790" + integrity sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ== + dependencies: + moment "^2.29.4" + moment@2.29.4, moment@2.x, "moment@>= 2.9.0", moment@^2.29.4: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" @@ -12752,6 +12837,18 @@ rc-cascader@3.12.1: rc-tree "~5.7.0" rc-util "^5.6.1" +rc-cascader@3.18.1: + version "3.18.1" + resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.18.1.tgz#e488e9cd9ace1617e06ee4c8eadf435a11de2d29" + integrity sha512-M7Xr5Fs/E87ZGustfObtBYQjsvBCET0UX2JYXB2GmOP+2fsZgjaRGXK+CJBmmWXQ6o4OFinpBQBXG4wJOQ5MEg== + dependencies: + "@babel/runtime" "^7.12.5" + array-tree-filter "^2.1.0" + classnames "^2.3.1" + rc-select "~14.9.0" + rc-tree "~5.7.0" + rc-util "^5.35.0" + rc-cascader@3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.7.0.tgz#98134df578ce1cca22be8fb4319b04df4f3dca36" @@ -12785,17 +12882,6 @@ rc-drawer@4.4.3: classnames "^2.2.6" rc-util "^5.7.0" -rc-drawer@6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-6.1.2.tgz#032918a21bfa8a7d9e52ada1e7b8ed08c0ae6346" - integrity sha512-mYsTVT8Amy0LRrpVEv7gI1hOjtfMSO/qHAaCDzFx9QBLnms3cAQLJkaxRWM+Eq99oyLhU/JkgoqTg13bc4ogOQ== - dependencies: - "@babel/runtime" "^7.10.1" - "@rc-component/portal" "^1.0.0-6" - classnames "^2.2.6" - rc-motion "^2.6.1" - rc-util "^5.21.2" - rc-drawer@6.1.3: version "6.1.3" resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-6.1.3.tgz#4b2277db09f059be7144dc82d5afede9c2ab2191" @@ -12818,6 +12904,17 @@ rc-drawer@6.3.0: rc-motion "^2.6.1" rc-util "^5.21.2" +rc-drawer@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-6.5.2.tgz#49c1f279261992f6d4653d32a03b14acd436d610" + integrity sha512-QckxAnQNdhh4vtmKN0ZwDf3iakO83W9eZcSKWYYTDv4qcD2fHhRAZJJ/OE6v2ZlQ2kSqCJX5gYssF4HJFvsEPQ== + dependencies: + "@babel/runtime" "^7.10.1" + "@rc-component/portal" "^1.1.1" + classnames "^2.2.6" + rc-motion "^2.6.1" + rc-util "^5.36.0" + rc-motion@^2.0.0, rc-motion@^2.0.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.6.2.tgz#3d31f97e41fb8e4f91a4a4189b6a98ac63342869" @@ -12846,6 +12943,16 @@ rc-overflow@^1.0.0: rc-resize-observer "^1.0.0" rc-util "^5.19.2" +rc-overflow@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/rc-overflow/-/rc-overflow-1.3.2.tgz#72ee49e85a1308d8d4e3bd53285dc1f3e0bcce2c" + integrity sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-resize-observer "^1.0.0" + rc-util "^5.37.0" + rc-resize-observer@^1.0.0, rc-resize-observer@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.2.0.tgz#9f46052f81cdf03498be35144cb7c53fd282c4c7" @@ -12905,6 +13012,19 @@ rc-select@~14.5.0: rc-util "^5.16.1" rc-virtual-list "^3.5.2" +rc-select@~14.9.0: + version "14.9.2" + resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.9.2.tgz#24c4673e21b1d5a4a126b9a934609cce5c39d1a5" + integrity sha512-VQ15sRFgPURHb8ZcZNSDtb2rAw3+C9xlL0nDziwNHTEW1KvEpZ8y+0v5w24X/Bpl9b3cW1BOyW1F5UqSAq+7Dg== + dependencies: + "@babel/runtime" "^7.10.1" + "@rc-component/trigger" "^1.5.0" + classnames "2.x" + rc-motion "^2.0.1" + rc-overflow "^1.3.1" + rc-util "^5.16.1" + rc-virtual-list "^3.5.2" + rc-slider@10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.0.1.tgz#7058c68ff1e1aa4e7c3536e5e10128bdbccb87f9" @@ -12915,16 +13035,6 @@ rc-slider@10.0.1: rc-util "^5.18.1" shallowequal "^1.1.0" -rc-slider@10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.1.0.tgz#11e401d8412ae20f9c2ee478bdbaddd042158753" - integrity sha512-nhC8V0+lNj4gGKZix2QAfcj/EP3NvCtFhNJPFMvXUdn7pe8bSa2vXNSxQVN5b9veVSic4Xeqgd/7KamX3gqznA== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "^2.2.5" - rc-util "^5.18.1" - shallowequal "^1.1.0" - rc-slider@10.1.1: version "10.1.1" resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.1.1.tgz#5e82036e60b61021aba3ea0e353744dd7c74e104" @@ -12943,6 +13053,15 @@ rc-slider@10.2.1: classnames "^2.2.5" rc-util "^5.27.0" +rc-slider@10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.3.1.tgz#345e818975f4bb61b66340799af8cfccad7c8ad7" + integrity sha512-XszsZLkbjcG9ogQy/zUC0n2kndoKUAnY/Vnk1Go5Gx+JJQBz0Tl15d5IfSiglwBUZPS9vsUJZkfCmkIZSqWbcA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.27.0" + rc-table@^7.17.1: version "7.28.1" resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.28.1.tgz#a1116653e8ccd3ddd69379694da41c3ee9ced9ed" @@ -13083,6 +13202,14 @@ rc-util@^5.33.0, rc-util@^5.36.0: "@babel/runtime" "^7.18.3" react-is "^16.12.0" +rc-util@^5.35.0, rc-util@^5.37.0: + version "5.38.0" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.38.0.tgz#18a3d1c26ba3c43fabfbe6303e825dabd9e5f4f0" + integrity sha512-yV/YBNdFn+edyBpBdCqkPE29Su0jWcHNgwx2dJbRqMrMfrUcMJUjCRV+ZPhcvWyKFJ63GzEerPrz9JIVo0zXmA== + dependencies: + "@babel/runtime" "^7.18.3" + react-is "^18.2.0" + rc-virtual-list@^3.2.0, rc-virtual-list@^3.4.8: version "3.4.11" resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.4.11.tgz#97f5e947380d546a2ca8ad229d8e41e9b33b20c6" @@ -13209,16 +13336,7 @@ react-dev-utils@^12.0.0: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" - -react-dom@^18.0.0: +react-dom@18.2.0, react-dom@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -13328,7 +13446,7 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0: +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== @@ -13491,7 +13609,22 @@ react-select@5.7.0: react-transition-group "^4.3.0" use-isomorphic-layout-effect "^1.1.2" -react-shallow-renderer@^16.13.1: +react-select@5.7.4: + version "5.7.4" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.4.tgz#d8cad96e7bc9d6c8e2709bdda8f4363c5dd7ea7d" + integrity sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@floating-ui/dom" "^1.0.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^6.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + use-isomorphic-layout-effect "^1.1.2" + +react-shallow-renderer@^16.15.0: version "16.15.0" resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== @@ -13520,15 +13653,14 @@ react-table@7.8.0: resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2" integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA== -react-test-renderer@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" - integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== +react-test-renderer@^18.0.2: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.2.0.tgz#1dd912bd908ff26da5b9fca4fd1c489b9523d37e" + integrity sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA== dependencies: - object-assign "^4.1.1" - react-is "^17.0.2" - react-shallow-renderer "^16.13.1" - scheduler "^0.20.2" + react-is "^18.2.0" + react-shallow-renderer "^16.15.0" + scheduler "^0.23.0" react-transition-group@4.4.5, react-transition-group@^4.3.0, react-transition-group@^4.4.5: version "4.4.5" @@ -13573,15 +13705,15 @@ react-window@1.8.8: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react-window@1.8.9: + version "1.8.9" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.9.tgz#24bc346be73d0468cdf91998aac94e32bc7fa6a8" + integrity sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q== dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" -react@^18.0.0: +react@18.2.0, react@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== @@ -13978,6 +14110,13 @@ rxjs@7.8.0: dependencies: tslib "^2.1.0" +rxjs@7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + rxjs@^6.4.0, rxjs@^6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" @@ -14048,14 +14187,6 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" @@ -15268,6 +15399,11 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -15476,6 +15612,11 @@ uplot@1.6.24: resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.24.tgz#dfa213fa7da92763261920ea972ed1a5f9f6af12" integrity sha512-WpH2BsrFrqxkMu+4XBvc0eCDsRBhzoq9crttYeSI0bfxpzR5YoSVzZXOKFVWcVC7sp/aDXrdDPbDZGCtck2PVg== +uplot@1.6.26: + version "1.6.26" + resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.26.tgz#a6012fd141ad4a71741c75af0c71283d0ade45a7" + integrity sha512-qN0mveL6UsP40TnHzHAJkUQvpfA3y8zSLXtXKVlJo/sLfj2+vjan/Z3g81MCZjy/hEDUFNtnLftPmETDA4s7Rg== + upper-case-first@^1.1.0, upper-case-first@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115"