Alert group column/label selector (#3281)

# What this PR does

Adds new functionality to enable which columns should show on the alert
group page


![image](https://github.com/grafana/oncall/assets/40542072/952d4004-9cd6-478c-a104-cd5d270cfd58)

---------

Co-authored-by: Julia <ferril.darkdiver@gmail.com>
This commit is contained in:
Rares Mardare 2023-11-29 14:11:31 +02:00 committed by GitHub
parent ec1f120d9c
commit 455f74560c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 2695 additions and 1013 deletions

View file

@ -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))

View file

@ -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,
}

View file

@ -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

View file

@ -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"]

View file

@ -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",
),
]

View file

@ -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))

View file

@ -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
]

View file

@ -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),
),
]

View file

@ -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(

View file

@ -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)

View file

@ -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));
}

View file

@ -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() {}
},
});

View file

@ -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",

View file

@ -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;

View file

@ -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;
}

View file

@ -27,7 +27,7 @@ const CheatSheet = (props: CheatSheetProps) => {
<VerticalGroup>
<HorizontalGroup justify="space-between">
<Text strong>{cheatSheetName} cheatsheet</Text>
<IconButton name="times" onClick={onClose} />
<IconButton aria-label="Close" name="times" onClick={onClose} />
</HorizontalGroup>
<Text type="secondary">{cheatSheetData.description}</Text>
<div className={cx('u-width-100')}>
@ -70,7 +70,7 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => {
{item.codeExample}
</Text>
<CopyToClipboard text={item.codeExample} onCopy={() => openNotification('Example copied')}>
<IconButton name="copy" />
<IconButton aria-label="Copy" name="copy" />
</CopyToClipboard>
</HorizontalGroup>
</Block>

View file

@ -115,7 +115,7 @@ const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props: Props<R
if (rowSelection) {
columns.unshift({
width: '25px',
width: '40px',
key: 'check',
title: (
<Checkbox

View file

@ -112,7 +112,7 @@ const IntegrationCollapsibleTreeItem: React.FC<{
<div className={cx('integrationTree__group', { 'integrationTree__group--hidden': item.isHidden })}>
<div className={cx('integrationTree__icon')}>
{item.canHoverIcon ? (
<IconButton name={getIconName()} onClick={iconOnClickFn} size="lg" />
<IconButton aria-label="" name={getIconName()} onClick={iconOnClickFn} size="lg" />
) : (
<Icon name={getIconName()} onClick={iconOnClickFn} size="lg" />
)}

View file

@ -256,6 +256,7 @@ const IntegrationContactPoint: React.FC<{
return (
<HorizontalGroup spacing="md">
<IconButton
aria-label="Alert Manager"
name="external-link-alt"
onClick={() => {
window.open(
@ -277,6 +278,7 @@ const IntegrationContactPoint: React.FC<{
}
>
<IconButton
aria-label="Disconnect Contact Point"
name="trash-alt"
onClick={() => {
alertReceiveChannelStore

View file

@ -35,13 +35,13 @@ const IntegrationInputField: React.FC<IntegrationInputFieldProps> = ({
<div className={cx('icons')}>
<HorizontalGroup spacing={'xs'}>
{showEye && <IconButton name={'eye'} size={'xs'} onClick={onInputReveal} />}
{showEye && <IconButton aria-label="Reveal" name={'eye'} size={'xs'} onClick={onInputReveal} />}
{showCopy && (
<CopyToClipboard text={value} onCopy={onCopy}>
<IconButton name={'copy'} size={'xs'} />
<IconButton aria-label="Copy" name={'copy'} size={'xs'} />
</CopyToClipboard>
)}
{showExternal && <IconButton name={'external-link-alt'} size={'xs'} onClick={onOpen} />}
{showExternal && <IconButton aria-label="Open" name={'external-link-alt'} size={'xs'} onClick={onOpen} />}
</HorizontalGroup>
</div>
</div>

View file

@ -72,6 +72,7 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
{this._renderControls(isDisabled)}
<WithPermissionControlTooltip userAction={userAction}>
<IconButton
aria-label="Remove"
className={cx('control')}
name="trash-alt"
onClick={this._getDeleteClickHandler(id)}

View file

@ -106,7 +106,11 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
Calculation methodology
</Text>
</HorizontalGroup>
<IconButton name={expanded ? 'arrow-down' : 'arrow-right'} onClick={handleExpandClick} />
<IconButton
aria-label={expanded ? 'Collapse' : 'Expand'}
name={expanded ? 'arrow-down' : 'arrow-right'}
onClick={handleExpandClick}
/>
</HorizontalGroup>
{expanded && (
<Text type="primary" className={cx('text')}>

View file

@ -33,7 +33,13 @@ const SourceCode: FC<SourceCodeProps> = (props) => {
>
{showClipboardIconOnly ? (
<Tooltip placement="top" content="Copy to Clipboard">
<IconButton className={cx('copyIcon')} size={'lg'} name="copy" data-testid="test__copyIcon" />
<IconButton
aria-label="Copy"
className={cx('copyIcon')}
size={'lg'}
name="copy"
data-testid="test__copyIcon"
/>
</Tooltip>
) : (
<Button

View file

@ -5,7 +5,7 @@ exports[`Unauthorized renders properly - access control enabled: false 1`] = `
className="not-found"
>
<div
className="css-9ztktj-vertical-group"
className="css-8tu8mo-vertical-group"
style={
Object {
"height": "100%",
@ -14,7 +14,7 @@ exports[`Unauthorized renders properly - access control enabled: false 1`] = `
}
>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h1
className="title error-code"
@ -32,7 +32,7 @@ exports[`Unauthorized renders properly - access control enabled: false 1`] = `
</h1>
</div>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h4
className="title"
@ -63,7 +63,7 @@ exports[`Unauthorized renders properly - access control enabled: true 1`] = `
className="not-found"
>
<div
className="css-9ztktj-vertical-group"
className="css-8tu8mo-vertical-group"
style={
Object {
"height": "100%",
@ -72,7 +72,7 @@ exports[`Unauthorized renders properly - access control enabled: true 1`] = `
}
>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h1
className="title error-code"
@ -90,7 +90,7 @@ exports[`Unauthorized renders properly - access control enabled: true 1`] = `
</h1>
</div>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h4
className="title"
@ -121,7 +121,7 @@ exports[`Unauthorized renders properly the grammar for different roles - Admin 1
className="not-found"
>
<div
className="css-9ztktj-vertical-group"
className="css-8tu8mo-vertical-group"
style={
Object {
"height": "100%",
@ -130,7 +130,7 @@ exports[`Unauthorized renders properly the grammar for different roles - Admin 1
}
>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h1
className="title error-code"
@ -148,7 +148,7 @@ exports[`Unauthorized renders properly the grammar for different roles - Admin 1
</h1>
</div>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h4
className="title"
@ -179,7 +179,7 @@ exports[`Unauthorized renders properly the grammar for different roles - Editor
className="not-found"
>
<div
className="css-9ztktj-vertical-group"
className="css-8tu8mo-vertical-group"
style={
Object {
"height": "100%",
@ -188,7 +188,7 @@ exports[`Unauthorized renders properly the grammar for different roles - Editor
}
>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h1
className="title error-code"
@ -206,7 +206,7 @@ exports[`Unauthorized renders properly the grammar for different roles - Editor
</h1>
</div>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h4
className="title"
@ -237,7 +237,7 @@ exports[`Unauthorized renders properly the grammar for different roles - Viewer
className="not-found"
>
<div
className="css-9ztktj-vertical-group"
className="css-8tu8mo-vertical-group"
style={
Object {
"height": "100%",
@ -246,7 +246,7 @@ exports[`Unauthorized renders properly the grammar for different roles - Viewer
}
>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h1
className="title error-code"
@ -264,7 +264,7 @@ exports[`Unauthorized renders properly the grammar for different roles - Viewer
</h1>
</div>
<div
className="css-1ec9088-layoutChildrenWrapper"
className="css-qxdyop-layoutChildrenWrapper"
>
<h4
className="title"

View file

@ -26,7 +26,7 @@ interface UserGroupsProps {
const cx = cn.bind(styles);
const DragHandle = () => <IconButton className={cx('icon')} name="draggabledots" />;
const DragHandle = () => <IconButton aria-label="Drag" className={cx('icon')} name="draggabledots" />;
const SortableHandleHoc = SortableHandle(DragHandle);
@ -101,7 +101,12 @@ const UserGroups = (props: UserGroupsProps) => {
{!disabled && (
<div className={cx('user-buttons')}>
<HorizontalGroup>
<IconButton className={cx('icon')} name="trash-alt" onClick={getDeleteItemHandler(index)} />
<IconButton
aria-label="Remove"
className={cx('icon')}
name="trash-alt"
onClick={getDeleteItemHandler(index)}
/>
<SortableHandleHoc />
</HorizontalGroup>
</div>

View file

@ -5,6 +5,8 @@ import { ContextMenu } from '@grafana/ui';
export interface WithContextMenuProps {
children: (props: { openMenu: React.MouseEventHandler<HTMLElement> }) => JSX.Element;
renderMenuItems: ({ closeMenu }: { closeMenu?: () => void }) => React.ReactNode;
isOpen?: boolean;
forceIsOpen?: boolean;
focusOnOpen?: boolean;
}

View file

@ -9,11 +9,11 @@ exports[`AddResponders should properly display the add responders button when hi
class="root root_bordered"
>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<h4
class="title"
@ -26,30 +26,18 @@ exports[`AddResponders should properly display the add responders button when hi
</h4>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div>
<button
class="css-b2ba3d-button"
class="css-8b29hm-button"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-1gebccs"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19,11H13V5a1,1,0,0,0-2,0v6H5a1,1,0,0,0,0,2h6v6a1,1,0,0,0,2,0V13h6a1,1,0,0,0,0-2Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Invite
</span>
@ -74,11 +62,11 @@ exports[`AddResponders should properly display the add responders button when hi
class="root root_bordered"
>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<h4
class="title"
@ -108,11 +96,11 @@ exports[`AddResponders should render properly in create mode 1`] = `
class="root root_bordered"
>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<h4
class="title"
@ -125,30 +113,18 @@ exports[`AddResponders should render properly in create mode 1`] = `
</h4>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div>
<button
class="css-b2ba3d-button"
class="css-8b29hm-button"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-1gebccs"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19,11H13V5a1,1,0,0,0-2,0v6H5a1,1,0,0,0,0,2h6v6a1,1,0,0,0,2,0V13h6a1,1,0,0,0,0-2Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Invite
</span>
@ -173,11 +149,11 @@ exports[`AddResponders should render properly in update mode 1`] = `
class="root root_bordered"
>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<h4
class="title"
@ -190,30 +166,18 @@ exports[`AddResponders should render properly in update mode 1`] = `
</h4>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div>
<button
class="css-b2ba3d-button"
class="css-8b29hm-button"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-1gebccs"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19,11H13V5a1,1,0,0,0-2,0v6H5a1,1,0,0,0,0,2h6v6a1,1,0,0,0,2,0V13h6a1,1,0,0,0,0-2Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Add
</span>
@ -238,11 +202,11 @@ exports[`AddResponders should render selected team and users properly 1`] = `
class="root root_bordered"
>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<h4
class="title"
@ -255,30 +219,18 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</h4>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div>
<button
class="css-b2ba3d-button"
class="css-8b29hm-button"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-1gebccs"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19,11H13V5a1,1,0,0,0-2,0v6H5a1,1,0,0,0,0,2h6v6a1,1,0,0,0,2,0V13h6a1,1,0,0,0,0-2Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Invite
</span>
@ -291,18 +243,18 @@ exports[`AddResponders should render selected team and users properly 1`] = `
>
<li>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="timeline-icon-background"
@ -315,7 +267,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<span
class="root text text--undefined text--medium responder-name"
@ -326,47 +278,36 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
aria-label="Remove responder"
class="css-x1vujn"
class="css-17584xm"
data-testid="team-responder-delete-icon"
tabindex="0"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-hj6vlq"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10,18a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,10,18ZM20,6H16V5a3,3,0,0,0-3-3H11A3,3,0,0,0,8,5V6H4A1,1,0,0,0,4,8H5V19a3,3,0,0,0,3,3h8a3,3,0,0,0,3-3V8h1a1,1,0,0,0,0-2ZM10,5a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1V6H10Zm7,14a1,1,0,0,1-1,1H8a1,1,0,0,1-1-1V8H17Zm-3-1a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,14,18Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</button>
</div>
</div>
</li>
<li>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="timeline-icon-background timeline-icon-background--green"
@ -379,7 +320,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<span
class="root text text--undefined text--medium responder-name"
@ -390,17 +331,17 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-19xfdrs-input-wrapper select css-8k5qe3-SelectContainer"
class="css-e49k3t-input-wrapper select css-8k5qe3-SelectContainer"
>
<span
class="css-1f43avz-a11yText-A11yText"
@ -413,16 +354,16 @@ exports[`AddResponders should render selected team and users properly 1`] = `
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="css-1scgfe8"
class="css-1i88p6p"
>
<div
class="css-1kl463j-grafana-select-value-container"
class="css-1q0c0d5-grafana-select-value-container"
>
<div
class=" css-1yc0yww-placeholder"
class=" css-1n8tjau-placeholder"
id="react-select-2-placeholder"
>
Choose
Select...
</div>
<input
aria-autocomplete="list"
@ -440,51 +381,28 @@ exports[`AddResponders should render selected team and users properly 1`] = `
/>
</div>
<div
class="css-uvldi-input-suffix"
class="css-zyjsuv-input-suffix"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-eyx4do"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17,9.17a1,1,0,0,0-1.41,0L12,12.71,8.46,9.17a1,1,0,0,0-1.41,0,1,1,0,0,0,0,1.42l4.24,4.24a1,1,0,0,0,1.42,0L17,10.59A1,1,0,0,0,17,9.17Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</div>
</div>
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
aria-label="Remove responder"
class="css-x1vujn"
class="css-17584xm"
data-testid="user-responder-delete-icon"
tabindex="0"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-hj6vlq"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10,18a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,10,18ZM20,6H16V5a3,3,0,0,0-3-3H11A3,3,0,0,0,8,5V6H4A1,1,0,0,0,4,8H5V19a3,3,0,0,0,3,3h8a3,3,0,0,0,3-3V8h1a1,1,0,0,0,0-2ZM10,5a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1V6H10Zm7,14a1,1,0,0,1-1,1H8a1,1,0,0,1-1-1V8H17Zm-3-1a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,14,18Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</button>
</div>
</div>
@ -493,18 +411,18 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</li>
<li>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="timeline-icon-background timeline-icon-background--green"
@ -517,7 +435,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<span
class="root text text--undefined text--medium responder-name"
@ -528,17 +446,17 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-15fuo2f-input-wrapper select css-8k5qe3-SelectContainer"
class="css-1nmqu8c-input-wrapper select css-8k5qe3-SelectContainer"
>
<span
class="css-1f43avz-a11yText-A11yText"
@ -551,16 +469,16 @@ exports[`AddResponders should render selected team and users properly 1`] = `
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="css-1scgfe8"
class="css-1i88p6p"
>
<div
class="css-1kl463j-grafana-select-value-container"
class="css-1q0c0d5-grafana-select-value-container"
>
<div
class=" css-1yc0yww-placeholder"
class=" css-1n8tjau-placeholder"
id="react-select-3-placeholder"
>
Choose
Select...
</div>
<input
aria-autocomplete="list"
@ -577,51 +495,28 @@ exports[`AddResponders should render selected team and users properly 1`] = `
/>
</div>
<div
class="css-uvldi-input-suffix"
class="css-zyjsuv-input-suffix"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-eyx4do"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17,9.17a1,1,0,0,0-1.41,0L12,12.71,8.46,9.17a1,1,0,0,0-1.41,0,1,1,0,0,0,0,1.42l4.24,4.24a1,1,0,0,0,1.42,0L17,10.59A1,1,0,0,0,17,9.17Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</div>
</div>
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
aria-label="Remove responder"
class="css-x1vujn"
class="css-17584xm"
data-testid="user-responder-delete-icon"
tabindex="0"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-hj6vlq"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10,18a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,10,18ZM20,6H16V5a3,3,0,0,0-3-3H11A3,3,0,0,0,8,5V6H4A1,1,0,0,0,4,8H5V19a3,3,0,0,0,3,3h8a3,3,0,0,0,3-3V8h1a1,1,0,0,0,0-2ZM10,5a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1V6H10Zm7,14a1,1,0,0,1-1,1H8a1,1,0,0,1-1-1V8H17Zm-3-1a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,14,18Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</button>
</div>
</div>
@ -630,18 +525,18 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</li>
<li>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="timeline-icon-background timeline-icon-background--green"
@ -654,7 +549,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<span
class="root text text--undefined text--medium responder-name"
@ -665,17 +560,17 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-15fuo2f-input-wrapper select css-8k5qe3-SelectContainer"
class="css-1nmqu8c-input-wrapper select css-8k5qe3-SelectContainer"
>
<span
class="css-1f43avz-a11yText-A11yText"
@ -688,16 +583,16 @@ exports[`AddResponders should render selected team and users properly 1`] = `
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="css-1scgfe8"
class="css-1i88p6p"
>
<div
class="css-1kl463j-grafana-select-value-container"
class="css-1q0c0d5-grafana-select-value-container"
>
<div
class=" css-1yc0yww-placeholder"
class=" css-1n8tjau-placeholder"
id="react-select-4-placeholder"
>
Choose
Select...
</div>
<input
aria-autocomplete="list"
@ -714,51 +609,28 @@ exports[`AddResponders should render selected team and users properly 1`] = `
/>
</div>
<div
class="css-uvldi-input-suffix"
class="css-zyjsuv-input-suffix"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-eyx4do"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17,9.17a1,1,0,0,0-1.41,0L12,12.71,8.46,9.17a1,1,0,0,0-1.41,0,1,1,0,0,0,0,1.42l4.24,4.24a1,1,0,0,0,1.42,0L17,10.59A1,1,0,0,0,17,9.17Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</div>
</div>
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
aria-label="Remove responder"
class="css-x1vujn"
class="css-17584xm"
data-testid="user-responder-delete-icon"
tabindex="0"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-hj6vlq"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10,18a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,10,18ZM20,6H16V5a3,3,0,0,0-3-3H11A3,3,0,0,0,8,5V6H4A1,1,0,0,0,4,8H5V19a3,3,0,0,0,3,3h8a3,3,0,0,0,3-3V8h1a1,1,0,0,0,0-2ZM10,5a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1V6H10Zm7,14a1,1,0,0,1-1,1H8a1,1,0,0,1-1-1V8H17Zm-3-1a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,14,18Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</button>
</div>
</div>
@ -767,81 +639,63 @@ exports[`AddResponders should render selected team and users properly 1`] = `
</li>
<div
aria-label="[object Object]"
class="css-j2xd7x"
class="css-10yjoiw"
data-testid="data-testid Alert info"
role="status"
>
<div
class="css-38nxtd"
class="css-1td7znu"
>
<div
class="css-wf08df-Icon"
class="css-ufgc62"
>
<svg
class="css-eyx4do"
data-name="Layer 1"
height="24"
id="Layer_1"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
<div
class="css-tluiue"
>
<path
d="M12,2A10,10,0,1,0,22,12,10.01114,10.01114,0,0,0,12,2Zm0,18a8,8,0,1,1,8-8A8.00917,8.00917,0,0,1,12,20Zm0-8.5a1,1,0,0,0-1,1v3a1,1,0,0,0,2,0v-3A1,1,0,0,0,12,11.5Zm0-4a1.25,1.25,0,1,0,1.25,1.25A1.25,1.25,0,0,0,12,7.5Z"
<div
class="css-1j2891d-Icon"
/>
</svg>
</div>
</div>
</div>
<div
class="css-zmuccj"
>
<div
class="css-hui7p1"
class="css-1gmwkrf"
>
<span
class="root text text--primary text--medium"
class="css-9om60p"
>
<a
class="learn-more-link"
href="https://grafana.com/docs/oncall/latest/notify/#configure-user-notification-policies"
rel="noreferrer"
target="_blank"
<span
class="root text text--primary text--medium"
>
<span
class="root text text--link text--medium"
<a
class="learn-more-link"
href="https://grafana.com/docs/oncall/latest/notify/#configure-user-notification-policies"
rel="noreferrer"
target="_blank"
>
<div
class="css-ve64a7-horizontal-group"
style="width: 100%; height: 100%;"
<span
class="root text text--link text--medium"
>
<div
class="css-12pko5d-layoutChildrenWrapper"
>
Learn more
</div>
<div
class="css-12pko5d-layoutChildrenWrapper"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-wf08df-Icon"
class="css-12kn7ff-layoutChildrenWrapper"
>
<svg
class="css-eyx4do"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18,10.82a1,1,0,0,0-1,1V19a1,1,0,0,1-1,1H5a1,1,0,0,1-1-1V8A1,1,0,0,1,5,7h7.18a1,1,0,0,0,0-2H5A3,3,0,0,0,2,8V19a3,3,0,0,0,3,3H16a3,3,0,0,0,3-3V11.82A1,1,0,0,0,18,10.82Zm3.92-8.2a1,1,0,0,0-.54-.54A1,1,0,0,0,21,2H15a1,1,0,0,0,0,2h3.59L8.29,14.29a1,1,0,0,0,0,1.42,1,1,0,0,0,1.42,0L20,5.41V9a1,1,0,0,0,2,0V3A1,1,0,0,0,21.92,2.62Z"
/>
</svg>
Learn more
</div>
<div
class="css-12kn7ff-layoutChildrenWrapper"
>
<div
class="css-1j2891d-Icon"
/>
</div>
</div>
</div>
</span>
</a>
about Default vs Important user personal notification settings
</span>
</a>
about Default vs Important user personal notification settings
</span>
</span>
</div>
</div>

View file

@ -7,81 +7,79 @@ exports[`AddRespondersPopup it shows a loading message initially 1`] = `
data-testid="add-responders-popup"
>
<div
class="css-11uftlx-input-wrapper responders-filters"
class="css-qfli5h-input-wrapper responders-filters"
data-testid="input-wrapper"
>
<div
class="css-1w5c5dq-input-inputWrapper"
class="css-10lnb82-input-inputWrapper"
>
<input
class="css-1mlczho-input-input"
class="css-8tk2dk-input-input"
data-testid="add-responders-search-input"
placeholder="Search"
style="padding-right: 12px;"
value=""
/>
<div
class="css-7y3u6k-input-suffix"
class="css-7099m8-input-suffix"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-eyx4do"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.71,20.29,18,16.61A9,9,0,1,0,16.61,18l3.68,3.68a1,1,0,0,0,1.42,0A1,1,0,0,0,21.71,20.29ZM11,18a7,7,0,1,1,7-7A7,7,0,0,1,11,18Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</div>
</div>
</div>
<div
class="radio-buttons css-sv3u8u"
class="radio-buttons css-1nxrz2e"
role="radiogroup"
>
<input
checked=""
class="css-8hl977"
id="option-teams-radiogroup-1"
name="radiogroup-1"
type="radio"
/>
<label
class="css-1tpfx0m"
for="option-teams-radiogroup-1"
<div
class="css-1hvl7lx"
>
Teams
</label>
<input
class="css-8hl977"
id="option-users-radiogroup-1"
name="radiogroup-1"
type="radio"
/>
<label
class="css-1tpfx0m"
for="option-users-radiogroup-1"
<input
checked=""
class="css-1f9hgw3"
id="option-teams-radiogroup-1"
name="radiogroup-1"
type="radio"
/>
<label
class="css-10bka4u"
for="option-teams-radiogroup-1"
>
Teams
</label>
</div>
<div
class="css-1hvl7lx"
>
Users
</label>
<input
class="css-1f9hgw3"
id="option-users-radiogroup-1"
name="radiogroup-1"
type="radio"
/>
<label
class="css-10bka4u"
for="option-users-radiogroup-1"
>
Users
</label>
</div>
</div>
<div
class="css-lq6a48 loading-placeholder"
class="css-1yjvs5a loading-placeholder"
>
Loading...
<div
class="css-13pg8vy"
class="css-1tqtz24"
data-testid="Spinner"
>
<i
aria-label="loading spinner"
class="fa fa-spinner fa-spin fa-spin"
/>
</div>

View file

@ -3,7 +3,7 @@
exports[`NotificationPoliciesSelect disabled state 1`] = `
<div>
<div
class="css-19xfdrs-input-wrapper select css-8k5qe3-SelectContainer"
class="css-e49k3t-input-wrapper select css-8k5qe3-SelectContainer"
>
<span
class="css-1f43avz-a11yText-A11yText"
@ -16,13 +16,13 @@ exports[`NotificationPoliciesSelect disabled state 1`] = `
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="css-1scgfe8"
class="css-1i88p6p"
>
<div
class="css-1kl463j-grafana-select-value-container"
class="css-1q0c0d5-grafana-select-value-container"
>
<div
class="css-3hgwt1-singleValue css-upz218-SingleValue"
class="css-sr1xkh-singleValue css-upz218-SingleValue"
>
Default
</div>
@ -41,23 +41,11 @@ exports[`NotificationPoliciesSelect disabled state 1`] = `
/>
</div>
<div
class="css-uvldi-input-suffix"
class="css-zyjsuv-input-suffix"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-eyx4do"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17,9.17a1,1,0,0,0-1.41,0L12,12.71,8.46,9.17a1,1,0,0,0-1.41,0,1,1,0,0,0,0,1.42l4.24,4.24a1,1,0,0,0,1.42,0L17,10.59A1,1,0,0,0,17,9.17Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</div>
</div>
</div>
@ -67,7 +55,7 @@ exports[`NotificationPoliciesSelect disabled state 1`] = `
exports[`NotificationPoliciesSelect it renders properly 1`] = `
<div>
<div
class="css-15fuo2f-input-wrapper select css-8k5qe3-SelectContainer"
class="css-1nmqu8c-input-wrapper select css-8k5qe3-SelectContainer"
>
<span
class="css-1f43avz-a11yText-A11yText"
@ -80,13 +68,13 @@ exports[`NotificationPoliciesSelect it renders properly 1`] = `
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="css-1scgfe8"
class="css-1i88p6p"
>
<div
class="css-1kl463j-grafana-select-value-container"
class="css-1q0c0d5-grafana-select-value-container"
>
<div
class="css-144maed-singleValue css-upz218-SingleValue"
class="css-8nwx1l-singleValue css-upz218-SingleValue"
>
Default
</div>
@ -104,23 +92,11 @@ exports[`NotificationPoliciesSelect it renders properly 1`] = `
/>
</div>
<div
class="css-uvldi-input-suffix"
class="css-zyjsuv-input-suffix"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-eyx4do"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17,9.17a1,1,0,0,0-1.41,0L12,12.71,8.46,9.17a1,1,0,0,0-1.41,0,1,1,0,0,0,0,1.42l4.24,4.24a1,1,0,0,0,1.42,0L17,10.59A1,1,0,0,0,17,9.17Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</div>
</div>
</div>

View file

@ -4,18 +4,18 @@ exports[`TeamResponder it renders data properly 1`] = `
<div>
<li>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="timeline-icon-background"
@ -28,7 +28,7 @@ exports[`TeamResponder it renders data properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<span
class="root text text--undefined text--medium responder-name"
@ -39,29 +39,18 @@ exports[`TeamResponder it renders data properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
aria-label="Remove responder"
class="css-x1vujn"
class="css-17584xm"
data-testid="team-responder-delete-icon"
tabindex="0"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-hj6vlq"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10,18a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,10,18ZM20,6H16V5a3,3,0,0,0-3-3H11A3,3,0,0,0,8,5V6H4A1,1,0,0,0,4,8H5V19a3,3,0,0,0,3,3h8a3,3,0,0,0,3-3V8h1a1,1,0,0,0,0-2ZM10,5a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1V6H10Zm7,14a1,1,0,0,1-1,1H8a1,1,0,0,1-1-1V8H17Zm-3-1a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,14,18Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</button>
</div>
</div>

View file

@ -4,18 +4,18 @@ exports[`UserResponder it renders data properly 1`] = `
<div>
<li>
<div
class="css-on8nbh-horizontal-group"
class="css-1mhys9y-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="timeline-icon-background timeline-icon-background--green"
@ -28,7 +28,7 @@ exports[`UserResponder it renders data properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<span
class="root text text--undefined text--medium responder-name"
@ -39,17 +39,17 @@ exports[`UserResponder it renders data properly 1`] = `
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<div
class="css-15fuo2f-input-wrapper select css-8k5qe3-SelectContainer"
class="css-1nmqu8c-input-wrapper select css-8k5qe3-SelectContainer"
>
<span
class="css-1f43avz-a11yText-A11yText"
@ -62,13 +62,13 @@ exports[`UserResponder it renders data properly 1`] = `
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="css-1scgfe8"
class="css-1i88p6p"
>
<div
class="css-1kl463j-grafana-select-value-container"
class="css-1q0c0d5-grafana-select-value-container"
>
<div
class="css-144maed-singleValue css-upz218-SingleValue"
class="css-8nwx1l-singleValue css-upz218-SingleValue"
>
Important
</div>
@ -86,51 +86,28 @@ exports[`UserResponder it renders data properly 1`] = `
/>
</div>
<div
class="css-uvldi-input-suffix"
class="css-zyjsuv-input-suffix"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-eyx4do"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17,9.17a1,1,0,0,0-1.41,0L12,12.71,8.46,9.17a1,1,0,0,0-1.41,0,1,1,0,0,0,0,1.42l4.24,4.24a1,1,0,0,0,1.42,0L17,10.59A1,1,0,0,0,17,9.17Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</div>
</div>
</div>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
aria-label="Remove responder"
class="css-x1vujn"
class="css-17584xm"
data-testid="user-responder-delete-icon"
tabindex="0"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-hj6vlq"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10,18a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,10,18ZM20,6H16V5a3,3,0,0,0-3-3H11A3,3,0,0,0,8,5V6H4A1,1,0,0,0,4,8H5V19a3,3,0,0,0,3,3h8a3,3,0,0,0,3-3V8h1a1,1,0,0,0,0-2ZM10,5a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1V6H10Zm7,14a1,1,0,0,1-1,1H8a1,1,0,0,1-1-1V8H17Zm-3-1a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,14,18Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</button>
</div>
</div>

View file

@ -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;
`,
};
};

View file

@ -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<ColumnRowProps> = ({ column, onItemChange, onColumnRemoval }) => {
const dnd = useSortable({ id: column.id });
const styles = useStyles2(getColumnsSelectorStyles);
const { attributes, listeners, setNodeRef, transform, transition } = dnd;
const columnElRef = useRef<HTMLDivElement>(undefined);
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={{ ...style }} className={styles.columnRow}>
<div className={styles.columnItem} ref={columnElRef}>
<span className={styles.columnName}>{column.name}</span>
{column.type === AlertGroupColumnType.LABEL && (
<Tooltip content="Label Column">
<Icon aria-label="Label" name="tag-alt" className={styles.labelIcon} />
</Tooltip>
)}
<RenderConditionally shouldRender={column.isVisible}>
<IconButton
aria-label="Drag"
name="draggabledots"
className={styles.columnsIcon}
{...attributes}
{...listeners}
/>
</RenderConditionally>
<RenderConditionally shouldRender={!column.isVisible && column.type === AlertGroupColumnType.LABEL}>
<WithPermissionControlTooltip userAction={UserActions.OtherSettingsWrite}>
<IconButton
className={[styles.columnsIcon, styles.columnsIconTrash, 'columns-icon-trash'].join(' ')}
name="trash-alt"
aria-label="Remove"
tooltip={'Remove column'}
onClick={() => onColumnRemoval(column)}
/>
</WithPermissionControlTooltip>
</RenderConditionally>
</div>
<Checkbox
className={styles.columnsCheckbox}
type="checkbox"
value={column.isVisible}
onChange={() => onItemChange(column.id)}
/>
</div>
);
};
interface ColumnsSelectorProps {
onColumnAddModalOpen(): void;
onConfirmRemovalModalOpen(column: AlertGroupColumn): void;
}
export const ColumnsSelector: React.FC<ColumnsSelectorProps> = 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 (
<div className={styles.columnsSelectorView}>
<Text type="primary" className={styles.columnsHeader}>
Columns Settings
</Text>
<div className={styles.columnsVisibleSection}>
<Text type="primary" className={styles.columnsHeaderSmall}>
Visible ({visibleColumns.length})
</Text>
<DndContext
autoScroll={{ layoutShiftCompensation: false }}
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={(ev) => handleDragEnd(ev, true)}
>
<SortableContext items={columns} strategy={verticalListSortingStrategy}>
<TransitionGroup>
{visibleColumns.map((column) => (
<CSSTransition key={column.id} timeout={TRANSITION_MS} unmountOnExit classNames="fade">
<ColumnRow
key={column.id}
column={column}
onItemChange={onItemChange}
onColumnRemoval={onConfirmRemovalModalOpen}
/>
</CSSTransition>
))}
</TransitionGroup>
</SortableContext>
</DndContext>
</div>
<div className={styles.columnsHiddenSection}>
<Text type="primary" className={styles.columnsHeaderSmall}>
Hidden ({hiddenColumns.length})
</Text>
<DndContext
autoScroll={{ layoutShiftCompensation: false }}
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={(ev) => handleDragEnd(ev, false)}
>
<SortableContext items={columns} strategy={verticalListSortingStrategy}>
<TransitionGroup>
{hiddenColumns.map((column) => (
<CSSTransition key={column.id} timeout={TRANSITION_MS} classNames="fade">
<ColumnRow
key={column.id}
column={column}
onItemChange={onItemChange}
onColumnRemoval={onConfirmRemovalModalOpen}
/>
</CSSTransition>
))}
</TransitionGroup>
</SortableContext>
</DndContext>
</div>
<div className={styles.columnsSelectorButtons}>
<Button
variant={'secondary'}
tooltipPlacement="top"
tooltip={'Reset table to default columns'}
disabled={isResetLoading || isDefaultColumnOrder}
onClick={WrapAutoLoadingState(onReset, ActionKey.RESET_COLUMNS_FROM_ALERT_GROUP)}
>
{isResetLoading ? <LoadingPlaceholder text="Loading..." className="loadingPlaceholder" /> : 'Reset'}
</Button>
<WithPermissionControlTooltip userAction={UserActions.OtherSettingsWrite}>
<Button variant={'primary'} disabled={isResetLoading} icon="plus" onClick={onColumnAddModalOpen}>
Add
</Button>
</WithPermissionControlTooltip>
</div>
</div>
);
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;
}

View file

@ -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<ApiSchemas['LabelKey']>;
setIsModalOpen: (value: boolean) => void;
inputRef: React.RefObject<HTMLInputElement>;
}
interface SearchResult extends Pick<components['schemas']['LabelKey'], 'id' | 'name'> {
isChecked: boolean;
isCollapsed: boolean;
values: any[];
}
const DEBOUNCE_MS = 300;
export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => {
const store = useStore();
const styles = useStyles2(getColumnsSelectorWrapperStyles);
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
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 (
<Modal isOpen={isModalOpen} title={'Add column'} onDismiss={onCloseModal} closeOnEscape={false}>
<VerticalGroup spacing="md">
<div className={styles.content}>
<VerticalGroup spacing="md">
<Input
className={styles.input}
autoFocus
placeholder="Search..."
ref={inputRef}
onChange={debouncedOnInputChange}
/>
{inputRef?.current?.value === '' && (
<Text type="primary">
{availableKeysForSearching.length} {pluralize('item', availableKeysForSearching.length)} available.
Type to see suggestions
</Text>
)}
{inputRef?.current?.value && searchResults.length && (
<VerticalGroup spacing="xs">
{searchResults.map((result, index) => (
<VerticalGroup key={index}>
<div className={styles.fieldRow}>
<IconButton
aria-label={result.isCollapsed ? 'Expand' : 'Collapse'}
name={result.isCollapsed ? 'angle-right' : 'angle-down'}
onClick={() => expandOrCollapseSearchResultItem(result, index)}
/>
<Checkbox
type="checkbox"
className={styles.checkboxAddOption}
value={result.isChecked}
onChange={() => {
setSearchResults((items) => {
return items.map((item) => {
const updatedItem: SearchResult = { ...item, isChecked: !item.isChecked };
return item.id === result.id ? updatedItem : item;
});
});
}}
/>
<Text type="primary">{result.name}</Text>
</div>
{!result.isCollapsed && (
<Block bordered withBackground fullWidth className={styles.valuesBlock}>
{result.values === undefined ? (
<LoadingPlaceholder text="Loading..." className="loadingPlaceholder" />
) : (
renderLabelValues(result.name, result.values)
)}
</Block>
)}
</VerticalGroup>
))}
</VerticalGroup>
)}
{inputRef?.current?.value && searchResults.length === 0 && (
<Text type="primary">0 results for your search.</Text>
)}
</VerticalGroup>
</div>
<HorizontalGroup justify="flex-end" spacing="md">
<Button variant="secondary" onClick={onCloseModal}>
Close
</Button>
<WithPermissionControlTooltip userAction={UserActions.OtherSettingsWrite}>
<Button
disabled={isLoading || !searchResults.find((it) => it.isChecked)}
variant="primary"
onClick={onAddNewColumns}
>
{isLoading ? <LoadingPlaceholder className={'loader'} text="Loading..." /> : 'Add'}
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
</VerticalGroup>
</Modal>
);
function renderLabelValues(keyName: string, values: Array<ApiSchemas['LabelValue']>) {
return (
<HorizontalGroup spacing="xs">
{values.slice(0, 2).map((val) => (
<LabelTag label={keyName} value={val.name} key={val.id} />
))}
<div className={styles.totalValuesCount}>{values.length > 2 ? `+ ${values.length - 2}` : ``}</div>
</HorizontalGroup>
);
}
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();
}
}
);

View file

@ -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;
`,
};
};

View file

@ -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<ColumnsSelectorWrapperProps> = observer(() => {
const [isConfirmRemovalModalOpen, setIsConfirmRemovalModalOpen] = useState(false);
const [columnToBeRemoved, setColumnToBeRemoved] = useState<AlertGroupColumn>(undefined);
const [isColumnAddModalOpen, setIsColumnAddModalOpen] = useState(false);
const [isFloatingDisplayOpen, setIsFloatingDisplayOpen] = useState(false);
const [labelKeys, setLabelKeys] = useState<Array<ApiSchemas['LabelKey']>>([]);
const inputRef = useRef<HTMLInputElement>(null);
const wrappingFloatingContainerRef = useRef<HTMLDivElement>(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 (
<>
<ColumnsModal
inputRef={inputRef}
isModalOpen={isColumnAddModalOpen}
labelKeys={labelKeys}
setIsModalOpen={setIsColumnAddModalOpen}
/>
<Modal
closeOnEscape={false}
isOpen={isConfirmRemovalModalOpen}
title={'Remove column'}
onDismiss={onConfirmRemovalClose}
className={styles.removalModal}
>
<VerticalGroup spacing="lg">
<Text type="primary">Are you sure you want to remove column {columnToBeRemoved?.name}?</Text>
<HorizontalGroup justify="flex-end" spacing="md">
<Button variant={'secondary'} onClick={onConfirmRemovalClose}>
Cancel
</Button>
<WithPermissionControlTooltip userAction={UserActions.OtherSettingsWrite}>
<Button
disabled={isRemoveLoading}
variant={'destructive'}
onClick={WrapAutoLoadingState(onColumnRemovalClick, ActionKey.REMOVE_COLUMN_FROM_ALERT_GROUP)}
>
{isRemoveLoading ? <LoadingPlaceholder text="Loading..." className="loadingPlaceholder" /> : 'Remove'}
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
</VerticalGroup>
</Modal>
<div ref={wrappingFloatingContainerRef}>
{!isColumnAddModalOpen && !isConfirmRemovalModalOpen ? (
<div className={styles.floatingContainer}>
{renderToggletipButton()}
<div
className={[styles.floatingContent, isFloatingDisplayOpen ? styles.floatingContentVisible : ''].join(' ')}
>
<ColumnsSelector
onColumnAddModalOpen={() => setIsColumnAddModalOpen(!isColumnAddModalOpen)}
onConfirmRemovalModalOpen={(column: AlertGroupColumn) => {
setIsConfirmRemovalModalOpen(!isConfirmRemovalModalOpen);
setColumnToBeRemoved(column);
}}
/>
</div>
</div>
) : (
renderToggletipButton()
)}
</div>
</>
);
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<void> {
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 (
<Button
type="button"
variant={'secondary'}
icon="columns"
id="toggletip-button"
onClick={() => setIsFloatingDisplayOpen(!isFloatingDisplayOpen)}
>
<HorizontalGroup spacing="xs">
Columns
<Icon name="angle-down" />
</HorizontalGroup>
</Button>
);
}
});
function forceOpenToggletip() {
document.getElementById('toggletip-button')?.click();
}
export default ColumnsSelectorWrapper;

View file

@ -106,7 +106,12 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
{filters.map((filterOption: FilterOption) => (
<div key={filterOption.name} className={cx('filter')}>
<Text type="secondary">{capitalCase(filterOption.name)}:</Text> {this.renderFilterOption(filterOption)}
<IconButton size="sm" name="times" onClick={this.getDeleteFilterClickHandler(filterOption.name)} />
<IconButton
aria-label="Delete"
size="sm"
name="times"
onClick={this.getDeleteFilterClickHandler(filterOption.name)}
/>
</div>
))}
<Select

View file

@ -3,11 +3,11 @@
exports[`MobileAppConnection if we disconnect the app, it disconnects and fetches a new QR code 1`] = `
<div>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<div
class="container"
@ -16,11 +16,11 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
@ -29,7 +29,7 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -38,14 +38,14 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
@ -70,7 +70,7 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
</a>
</div>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
@ -102,11 +102,11 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
@ -115,7 +115,7 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -124,7 +124,7 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="u-width-100 u-flex u-flex-center u-position-relative"
@ -162,11 +162,11 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
exports[`MobileAppConnection it shows a QR code if the app isn't already connected 1`] = `
<div>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<div
class="container"
@ -175,11 +175,11 @@ exports[`MobileAppConnection it shows a QR code if the app isn't already connect
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
@ -188,7 +188,7 @@ exports[`MobileAppConnection it shows a QR code if the app isn't already connect
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -197,14 +197,14 @@ exports[`MobileAppConnection it shows a QR code if the app isn't already connect
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
@ -229,7 +229,7 @@ exports[`MobileAppConnection it shows a QR code if the app isn't already connect
</a>
</div>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
@ -261,15 +261,16 @@ exports[`MobileAppConnection it shows a QR code if the app isn't already connect
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-lq6a48"
class="css-1yjvs5a"
>
Loading...
<div
class="css-13pg8vy"
class="css-1tqtz24"
data-testid="Spinner"
>
<i
aria-label="loading spinner"
class="fa fa-spinner fa-spin fa-spin"
/>
</div>
@ -284,11 +285,11 @@ exports[`MobileAppConnection it shows a QR code if the app isn't already connect
exports[`MobileAppConnection it shows a loading message if it is currently disconnecting 1`] = `
<div>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<div
class="container"
@ -297,11 +298,11 @@ exports[`MobileAppConnection it shows a loading message if it is currently disco
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
@ -310,7 +311,7 @@ exports[`MobileAppConnection it shows a loading message if it is currently disco
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -319,14 +320,14 @@ exports[`MobileAppConnection it shows a loading message if it is currently disco
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
@ -351,7 +352,7 @@ exports[`MobileAppConnection it shows a loading message if it is currently disco
</a>
</div>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
@ -383,15 +384,16 @@ exports[`MobileAppConnection it shows a loading message if it is currently disco
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-lq6a48"
class="css-1yjvs5a"
>
Loading...
<div
class="css-13pg8vy"
class="css-1tqtz24"
data-testid="Spinner"
>
<i
aria-label="loading spinner"
class="fa fa-spinner fa-spin fa-spin"
/>
</div>
@ -406,11 +408,11 @@ exports[`MobileAppConnection it shows a loading message if it is currently disco
exports[`MobileAppConnection it shows a loading message if it is currently fetching the QR code 1`] = `
<div>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<div
class="container"
@ -419,11 +421,11 @@ exports[`MobileAppConnection it shows a loading message if it is currently fetch
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
@ -432,7 +434,7 @@ exports[`MobileAppConnection it shows a loading message if it is currently fetch
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -441,14 +443,14 @@ exports[`MobileAppConnection it shows a loading message if it is currently fetch
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
@ -473,7 +475,7 @@ exports[`MobileAppConnection it shows a loading message if it is currently fetch
</a>
</div>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
@ -505,15 +507,16 @@ exports[`MobileAppConnection it shows a loading message if it is currently fetch
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-lq6a48"
class="css-1yjvs5a"
>
Loading...
<div
class="css-13pg8vy"
class="css-1tqtz24"
data-testid="Spinner"
>
<i
aria-label="loading spinner"
class="fa fa-spinner fa-spin fa-spin"
/>
</div>
@ -528,11 +531,11 @@ exports[`MobileAppConnection it shows a loading message if it is currently fetch
exports[`MobileAppConnection it shows a message when the mobile app is already connected 1`] = `
<div>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<div
class="container"
@ -541,11 +544,11 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
@ -554,7 +557,7 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -563,14 +566,14 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
@ -595,7 +598,7 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
</a>
</div>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
@ -627,35 +630,23 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
>
App connected
<div
class="css-wf08df-Icon"
>
<svg
class="css-eyx4do icon"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.72,8.79l-4.29,4.3L8.78,11.44a1,1,0,1,0-1.41,1.41l2.35,2.36a1,1,0,0,0,.71.29,1,1,0,0,0,.7-.29l5-5a1,1,0,0,0,0-1.42A1,1,0,0,0,14.72,8.79ZM12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm0,18a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -664,7 +655,7 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="disconnect__container"
@ -674,12 +665,12 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
src="[object Object]"
/>
<button
class="css-1ed0qk5-button disconnect-button"
class="css-ttl745-button disconnect-button"
data-testid="test__disconnect"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Disconnect
</span>
@ -697,11 +688,11 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
exports[`MobileAppConnection it shows a warning when cloud is not connected 1`] = `
<div>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--secondary text--medium"
@ -710,33 +701,21 @@ exports[`MobileAppConnection it shows a warning when cloud is not connected 1`]
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<a
class="root"
href="/a/grafana-oncall-app/cloud"
>
<button
class="css-b2ba3d-button"
class="css-8b29hm-button"
type="button"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-1gebccs"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18,10.82a1,1,0,0,0-1,1V19a1,1,0,0,1-1,1H5a1,1,0,0,1-1-1V8A1,1,0,0,1,5,7h7.18a1,1,0,0,0,0-2H5A3,3,0,0,0,2,8V19a3,3,0,0,0,3,3H16a3,3,0,0,0,3-3V11.82A1,1,0,0,0,18,10.82Zm3.92-8.2a1,1,0,0,0-.54-.54A1,1,0,0,0,21,2H15a1,1,0,0,0,0,2h3.59L8.29,14.29a1,1,0,0,0,0,1.42,1,1,0,0,0,1.42,0L20,5.41V9a1,1,0,0,0,2,0V3A1,1,0,0,0,21.92,2.62Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Connect Grafana Cloud OnCall
</span>
@ -750,11 +729,11 @@ exports[`MobileAppConnection it shows a warning when cloud is not connected 1`]
exports[`MobileAppConnection it shows an error message if there was an error disconnecting the mobile app 1`] = `
<div>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<div
class="container"
@ -763,11 +742,11 @@ exports[`MobileAppConnection it shows an error message if there was an error dis
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
@ -776,7 +755,7 @@ exports[`MobileAppConnection it shows an error message if there was an error dis
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -785,14 +764,14 @@ exports[`MobileAppConnection it shows an error message if there was an error dis
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
@ -817,7 +796,7 @@ exports[`MobileAppConnection it shows an error message if there was an error dis
</a>
</div>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
@ -863,11 +842,11 @@ exports[`MobileAppConnection it shows an error message if there was an error dis
exports[`MobileAppConnection it shows an error message if there was an error fetching the QR code 1`] = `
<div>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<div
class="container"
@ -876,11 +855,11 @@ exports[`MobileAppConnection it shows an error message if there was an error fet
class="root root_bordered root_shadowed root--withBackGround container__box"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
@ -889,7 +868,7 @@ exports[`MobileAppConnection it shows an error message if there was an error fet
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -898,14 +877,14 @@ exports[`MobileAppConnection it shows an error message if there was an error fet
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
@ -930,7 +909,7 @@ exports[`MobileAppConnection it shows an error message if there was an error fet
</a>
</div>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"

View file

@ -3,12 +3,12 @@
exports[`DisconnectButton it renders properly 1`] = `
<div>
<button
class="css-1ed0qk5-button disconnect-button"
class="css-ttl745-button disconnect-button"
data-testid="test__disconnect"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Disconnect
</span>

View file

@ -3,11 +3,11 @@
exports[`DownloadIcons it renders properly 1`] = `
<div>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium text--strong"
@ -16,7 +16,7 @@ exports[`DownloadIcons it renders properly 1`] = `
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<span
class="root text text--primary text--medium"
@ -25,14 +25,14 @@ exports[`DownloadIcons it renders properly 1`] = `
</span>
</div>
<div
class="css-ztyofd-layoutChildrenWrapper"
class="css-12oo3x0-layoutChildrenWrapper"
>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
@ -57,7 +57,7 @@ exports[`DownloadIcons it renders properly 1`] = `
</a>
</div>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"

View file

@ -3,7 +3,7 @@
exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonData, and ONCALL_API_URL is passed in process.env, and there is an error calling selfHostedInstallPlugin, it sets an error message 1`] = `
<div>
<legend
class="css-11wqcat"
class="css-1w9pvsj"
>
Configure Grafana OnCall
</legend>
@ -20,32 +20,32 @@ exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonDa
</span>
</pre>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-z53gi5-button"
class="css-td06pi-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Retry Sync
</span>
</button>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-1ed0qk5-button"
class="css-ttl745-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Remove current configuration
</span>
@ -58,7 +58,7 @@ exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonDa
exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonData, or in process.env, updatePluginStatus is not called, and the configuration form is shown 1`] = `
<div>
<legend
class="css-11wqcat"
class="css-1w9pvsj"
>
Configure Grafana OnCall
</legend>
@ -66,7 +66,7 @@ exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonDa
This page will help you configure the OnCall plugin 👋
</p>
<form
class="css-xs0vux"
class="css-1srg48i"
data-testid="plugin-configuration-form"
>
<div
@ -112,42 +112,44 @@ exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonDa
</span>
</div>
<div
class="css-8e5b3"
class="css-1rplq84"
>
<div
class="css-jt4xma-Label"
class="css-l4ykjo-Label"
>
<label>
<div
class="css-xhqy0o"
class="css-70qvj9"
>
OnCall backend URL
</div>
</label>
</div>
<div>
<div
class="css-xcstkt-input-wrapper"
data-testid="input-wrapper"
>
<div>
<div
class="css-1w5c5dq-input-inputWrapper"
class="css-1cvb0sk-input-wrapper"
data-testid="input-wrapper"
>
<input
class="css-1mlczho-input-input"
data-testid="onCallApiUrl"
name="onCallApiUrl"
/>
<div
class="css-10lnb82-input-inputWrapper"
>
<input
class="css-8tk2dk-input-input"
data-testid="onCallApiUrl"
name="onCallApiUrl"
/>
</div>
</div>
</div>
</div>
</div>
<button
class="css-z53gi5-button"
class="css-td06pi-button"
type="submit"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Connect
</span>
@ -159,7 +161,7 @@ exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonDa
exports[`PluginConfigPage If onCallApiUrl is set, and updatePluginStatus returns an error, it sets an error message 1`] = `
<div>
<legend
class="css-11wqcat"
class="css-1w9pvsj"
>
Configure Grafana OnCall
</legend>
@ -176,32 +178,32 @@ exports[`PluginConfigPage If onCallApiUrl is set, and updatePluginStatus returns
</span>
</pre>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-z53gi5-button"
class="css-td06pi-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Retry Sync
</span>
</button>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-1ed0qk5-button"
class="css-ttl745-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Remove current configuration
</span>
@ -214,7 +216,7 @@ exports[`PluginConfigPage If onCallApiUrl is set, and updatePluginStatus returns
exports[`PluginConfigPage It doesn't make any network calls if the plugin configured query params are provided 1`] = `
<div>
<legend
class="css-11wqcat"
class="css-1w9pvsj"
>
Configure Grafana OnCall
</legend>
@ -228,33 +230,33 @@ exports[`PluginConfigPage It doesn't make any network calls if the plugin config
</span>
</pre>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<a
class="css-z53gi5-button"
class="css-td06pi-button"
href="/a/grafana-oncall-app/"
tabindex="0"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Open Grafana OnCall
</span>
</a>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-1ed0qk5-button"
class="css-ttl745-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Remove current configuration
</span>
@ -267,7 +269,7 @@ exports[`PluginConfigPage It doesn't make any network calls if the plugin config
exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnected does not return an error. It displays properly the plugin connected items based on the license - License: OpenSource 1`] = `
<div>
<legend
class="css-11wqcat"
class="css-1w9pvsj"
>
Configure Grafana OnCall
</legend>
@ -281,33 +283,33 @@ exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnec
</span>
</pre>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<a
class="css-z53gi5-button"
class="css-td06pi-button"
href="/a/grafana-oncall-app/"
tabindex="0"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Open Grafana OnCall
</span>
</a>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-1ed0qk5-button"
class="css-ttl745-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Remove current configuration
</span>
@ -320,7 +322,7 @@ exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnec
exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnected does not return an error. It displays properly the plugin connected items based on the license - License: some-other-license 1`] = `
<div>
<legend
class="css-11wqcat"
class="css-1w9pvsj"
>
Configure Grafana OnCall
</legend>
@ -334,18 +336,18 @@ exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnec
</span>
</pre>
<div
class="css-1j7sh2x-vertical-group"
class="css-gjl87o-vertical-group"
style="width: 100%; height: 100%;"
>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<div
class="css-jt4xma-Label"
class="css-l4ykjo-Label"
>
<label>
<div
class="css-xhqy0o"
class="css-70qvj9"
>
This is a cloud managed configuration.
</div>
@ -353,15 +355,15 @@ exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnec
</div>
</div>
<div
class="css-bxa289-layoutChildrenWrapper"
class="css-gxt817-layoutChildrenWrapper"
>
<a
class="css-z53gi5-button"
class="css-td06pi-button"
href="/a/grafana-oncall-app/"
tabindex="0"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Open Grafana OnCall
</span>
@ -374,7 +376,7 @@ exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnec
exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnected returns an error 1`] = `
<div>
<legend
class="css-11wqcat"
class="css-1w9pvsj"
>
Configure Grafana OnCall
</legend>
@ -391,32 +393,32 @@ exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnec
</span>
</pre>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-z53gi5-button"
class="css-td06pi-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Retry Sync
</span>
</button>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-1ed0qk5-button"
class="css-ttl745-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Remove current configuration
</span>
@ -429,7 +431,7 @@ exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnec
exports[`PluginConfigPage Plugin reset: successful - false 1`] = `
<div>
<legend
class="css-11wqcat"
class="css-1w9pvsj"
>
Configure Grafana OnCall
</legend>
@ -446,32 +448,32 @@ exports[`PluginConfigPage Plugin reset: successful - false 1`] = `
</span>
</pre>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-z53gi5-button"
class="css-td06pi-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Retry Sync
</span>
</button>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-1ed0qk5-button"
class="css-ttl745-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Remove current configuration
</span>
@ -484,7 +486,7 @@ exports[`PluginConfigPage Plugin reset: successful - false 1`] = `
exports[`PluginConfigPage Plugin reset: successful - true 1`] = `
<div>
<legend
class="css-11wqcat"
class="css-1w9pvsj"
>
Configure Grafana OnCall
</legend>
@ -492,7 +494,7 @@ exports[`PluginConfigPage Plugin reset: successful - true 1`] = `
This page will help you configure the OnCall plugin 👋
</p>
<form
class="css-xs0vux"
class="css-1srg48i"
data-testid="plugin-configuration-form"
>
<div
@ -538,42 +540,44 @@ exports[`PluginConfigPage Plugin reset: successful - true 1`] = `
</span>
</div>
<div
class="css-8e5b3"
class="css-1rplq84"
>
<div
class="css-jt4xma-Label"
class="css-l4ykjo-Label"
>
<label>
<div
class="css-xhqy0o"
class="css-70qvj9"
>
OnCall backend URL
</div>
</label>
</div>
<div>
<div
class="css-xcstkt-input-wrapper"
data-testid="input-wrapper"
>
<div>
<div
class="css-1w5c5dq-input-inputWrapper"
class="css-1cvb0sk-input-wrapper"
data-testid="input-wrapper"
>
<input
class="css-1mlczho-input-input"
data-testid="onCallApiUrl"
name="onCallApiUrl"
/>
<div
class="css-10lnb82-input-inputWrapper"
>
<input
class="css-8tk2dk-input-input"
data-testid="onCallApiUrl"
name="onCallApiUrl"
/>
</div>
</div>
</div>
</div>
</div>
<button
class="css-z53gi5-button"
class="css-td06pi-button"
type="submit"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Connect
</span>

View file

@ -4,7 +4,7 @@ exports[`ConfigurationForm It doesn't allow the user to submit if the URL is inv
<body>
<div>
<form
class="css-xs0vux"
class="css-1srg48i"
data-testid="plugin-configuration-form"
>
<div
@ -50,68 +50,58 @@ exports[`ConfigurationForm It doesn't allow the user to submit if the URL is inv
</span>
</div>
<div
class="css-8e5b3"
class="css-1rplq84"
>
<div
class="css-jt4xma-Label"
class="css-l4ykjo-Label"
>
<label>
<div
class="css-xhqy0o"
class="css-70qvj9"
>
OnCall backend URL
</div>
</label>
</div>
<div>
<div
class="css-1wz1ggz-input-wrapper"
data-testid="input-wrapper"
>
<div>
<div
class="css-1w5c5dq-input-inputWrapper"
class="css-1skhtb9-input-wrapper"
data-testid="input-wrapper"
>
<input
class="css-12lx392-input-input"
data-testid="onCallApiUrl"
name="onCallApiUrl"
/>
<div
class="css-10lnb82-input-inputWrapper"
>
<input
class="css-11mwy6h-input-input"
data-testid="onCallApiUrl"
name="onCallApiUrl"
/>
</div>
</div>
</div>
<div
class="css-1gd2lua"
class="css-1fhgjcy"
>
<div
class="css-mw0th3"
class="css-9z7wq3"
role="alert"
>
<div
class="css-wf08df-Icon"
>
<svg
class="css-1ah41zt"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12,16a1,1,0,1,0,1,1A1,1,0,0,0,12,16Zm10.67,1.47-8.05-14a3,3,0,0,0-5.24,0l-8,14A3,3,0,0,0,3.94,22H20.06a3,3,0,0,0,2.61-4.53Zm-1.73,2a1,1,0,0,1-.88.51H3.94a1,1,0,0,1-.88-.51,1,1,0,0,1,0-1l8-14a1,1,0,0,1,1.78,0l8.05,14A1,1,0,0,1,20.94,19.49ZM12,8a1,1,0,0,0-1,1v4a1,1,0,0,0,2,0V9A1,1,0,0,0,12,8Z"
/>
</svg>
</div>
class="css-1j2891d-Icon"
/>
Must be a valid URL
</div>
</div>
</div>
</div>
<button
class="css-z53gi5-button"
class="css-td06pi-button"
disabled=""
type="submit"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Connect
</span>
@ -125,7 +115,7 @@ exports[`ConfigurationForm It shows an error message if the self hosted plugin A
<body>
<div>
<form
class="css-xs0vux"
class="css-1srg48i"
data-testid="plugin-configuration-form"
>
<div
@ -171,32 +161,34 @@ exports[`ConfigurationForm It shows an error message if the self hosted plugin A
</span>
</div>
<div
class="css-8e5b3"
class="css-1rplq84"
>
<div
class="css-jt4xma-Label"
class="css-l4ykjo-Label"
>
<label>
<div
class="css-xhqy0o"
class="css-70qvj9"
>
OnCall backend URL
</div>
</label>
</div>
<div>
<div
class="css-xcstkt-input-wrapper"
data-testid="input-wrapper"
>
<div>
<div
class="css-1w5c5dq-input-inputWrapper"
class="css-1cvb0sk-input-wrapper"
data-testid="input-wrapper"
>
<input
class="css-1mlczho-input-input"
data-testid="onCallApiUrl"
name="onCallApiUrl"
/>
<div
class="css-10lnb82-input-inputWrapper"
>
<input
class="css-8tk2dk-input-input"
data-testid="onCallApiUrl"
name="onCallApiUrl"
/>
</div>
</div>
</div>
</div>
@ -263,11 +255,11 @@ exports[`ConfigurationForm It shows an error message if the self hosted plugin A
</span>
</div>
<button
class="css-z53gi5-button"
class="css-td06pi-button"
type="submit"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Connect
</span>

View file

@ -4,12 +4,12 @@ exports[`RemoveCurrentConfigurationButton It renders properly when disabled 1`]
<body>
<div>
<button
class="css-1ed0qk5-button"
class="css-ttl745-button"
disabled=""
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Remove current configuration
</span>
@ -22,11 +22,11 @@ exports[`RemoveCurrentConfigurationButton It renders properly when enabled 1`] =
<body>
<div>
<button
class="css-1ed0qk5-button"
class="css-ttl745-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Remove current configuration
</span>

View file

@ -452,7 +452,7 @@ const RotationForm = observer((props: RotationFormProps) => {
onClick={() => setShowDeleteRotationConfirmation(true)}
/>
)}
<IconButton variant="secondary" className={cx('drag-handler')} name="draggabledots" />
<IconButton aria-label="Drag" variant="secondary" className={cx('drag-handler')} name="draggabledots" />
<IconButton
name="times"
variant="secondary"

View file

@ -218,7 +218,7 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
<IconButton variant="secondary" tooltip="Delete" name="trash-alt" onClick={handleDeleteClick} />
</WithConfirm>
)}
<IconButton variant="secondary" className={cx('drag-handler')} name="draggabledots" />
<IconButton aria-label="Drag" variant="secondary" className={cx('drag-handler')} name="draggabledots" />
<IconButton
name="times"
variant="secondary"

View file

@ -158,7 +158,7 @@ const ShiftSwapForm = (props: ShiftSwapFormProps) => {
</WithConfirm>
</WithPermissionControlTooltip>
)}
<IconButton variant="secondary" className={cx('drag-handler')} name="draggabledots" />
<IconButton aria-label="Drag" variant="secondary" className={cx('drag-handler')} name="draggabledots" />
<IconButton name="times" variant="secondary" tooltip="Close" onClick={handleHide} />
</HorizontalGroup>
</HorizontalGroup>

View file

@ -170,7 +170,7 @@ const TeamModal = ({ teamId, onHide }: TeamModalProps) => {
{ label: 'Team members and admins', value: '0' },
]}
value={shareResourceToAll}
onChange={setShareResourceToAll}
onChange={setShareResourceToAll as (res: string) => void}
/>
</div>
</Field>

View file

@ -143,7 +143,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
<Text>Edit custom payload</Text>
<HorizontalGroup>
<IconButton name="times" onClick={returnToListView} />
<IconButton aria-label="List View" name="times" onClick={returnToListView} />
</HorizontalGroup>
</HorizontalGroup>
</div>
@ -263,7 +263,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
<Text>Edit custom payload</Text>
<HorizontalGroup>
<IconButton name="times" onClick={() => returnToListView()} />
<IconButton aria-label="List View" name="times" onClick={() => returnToListView()} />
</HorizontalGroup>
</HorizontalGroup>
</div>
@ -292,8 +292,8 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
<Text>{selectedTitle}</Text>
</div>
<div className={cx('title-action-icons')}>
<IconButton name="edit" onClick={() => setIsEditMode(true)} />
<IconButton name="times" onClick={() => returnToListView()} />
<IconButton aria-label="Edit" name="edit" onClick={() => setIsEditMode(true)} />
<IconButton aria-label="List View" name="times" onClick={() => returnToListView()} />
</div>
</div>
</div>

View file

@ -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<void> {
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<void> {
const method = isUserUpdate ? 'PUT' : 'POST';
const { default: isDefaultOrder } = await makeRequest('/alertgroup_table_settings', {
method,
data: { ...columns },
});
this.isDefaultColumnOrder = isDefaultOrder;
}
@action
async resetTableSettings(): Promise<void> {
return await makeRequest('/alertgroup_table_settings/reset', { method: 'POST' }).catch(() =>
openErrorNotification('There was an error resetting the table settings')
);
}
@action
async loadLabelsKeys(): Promise<Array<ApiSchemas['LabelKey']>> {
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<ApiSchemas['LabelValue']> }> {
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 };
}

View file

@ -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;

View file

@ -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',
}

View file

@ -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();

View file

@ -177,7 +177,7 @@ class EscalationChainsPage extends React.Component<EscalationChainsPageProps, Es
</GList>
) : (
<VerticalGroup>
<Text type="primary" className={cx('loading')}>
<Text type="primary" className={cx('loadingPlaceholder')}>
Loading...
</Text>
</VerticalGroup>

View file

@ -283,7 +283,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
<HorizontalGroup justify="space-between">
<HorizontalGroup className={cx('title')}>
<PluginLink query={{ page: 'alert-groups', ...query }}>
<IconButton name="arrow-left" size="xl" />
<IconButton aria-label="Go Back" name="arrow-left" size="xl" />
</PluginLink>
{/* @ts-ignore*/}
<HorizontalGroup align="baseline">
@ -765,7 +765,7 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle
<div className={cx('incident-row-right')}>
<HorizontalGroup wrap={false} justify={'flex-end'}>
<Tooltip placement="top" content="Alert Payload">
<IconButton name="arrow" onClick={() => openIncidentResponse(incident)} />
<IconButton aria-label="Alert Payload" name="arrow" onClick={() => openIncidentResponse(incident)} />
</Tooltip>
</HorizontalGroup>
</div>

View file

@ -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 {

View file

@ -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<string, any>;
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<IncidentsPageProps, IncidentsPageState> {
constructor(props: IncidentsPageProps) {
@ -82,16 +106,23 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
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<IncidentsPageProps, IncidentsPageState>
);
};
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<IncidentsPageProps, IncidentsPageState>
};
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<IncidentsPageProps, IncidentsPageState>
return (
<div className={cx('above-incidents-table')}>
<div className={cx('bulk-actions')}>
<HorizontalGroup>
<div className={cx('bulk-actions-container')}>
<div className={cx('bulk-actions-list')}>
{'resolve' in store.alertGroupStore.bulkActions && (
<WithPermissionControlTooltip key="resolve" userAction={UserActions.AlertGroupsWrite}>
<Button
@ -421,27 +457,42 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
? `${selectedIncidentIds.length} Alert Group${selectedIncidentIds.length > 1 ? 's' : ''} selected`
: 'No Alert Groups selected'}
</Text>
</HorizontalGroup>
</div>
{hasInvalidatedAlert && (
<div className={cx('out-of-date')}>
<Text type="secondary">Results out of date</Text>
<Button
style={{ marginLeft: '8px' }}
disabled={store.alertGroupStore.alertGroupsLoading}
variant="primary"
onClick={this.onIncidentsUpdateClick}
>
Refresh
</Button>
</div>
)}
<div className={cx('fields-dropdown')}>
<RenderConditionally shouldRender={hasInvalidatedAlert}>
<HorizontalGroup spacing="xs">
<Text type="secondary">Results out of date</Text>
<Button
className={cx('btn-results')}
disabled={store.alertGroupStore.alertGroupsLoading}
variant="primary"
onClick={this.onIncidentsUpdateClick}
>
Refresh
</Button>
</HorizontalGroup>
</RenderConditionally>
<RenderConditionally shouldRender={store.hasFeature(AppFeature.Labels)}>
<RadioButtonGroup
options={TABLE_SCROLL_OPTIONS}
value={isHorizontalScrolling}
onChange={this.onEnableHorizontalScroll}
/>
</RenderConditionally>
<RenderConditionally shouldRender={store.hasFeature(AppFeature.Labels)}>
<ColumnsSelectorWrapper />
</RenderConditionally>
</div>
</div>
</div>
);
};
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<IncidentsPageProps, IncidentsPageState>
);
}
const tableColumns = this.getTableColumns();
return (
<div className={cx('root')}>
{this.renderBulkActions()}
@ -481,7 +534,9 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
}}
rowKey="pk"
data={results}
columns={this.getTableColumns()}
columns={tableColumns}
tableLayout="auto"
scroll={{ x: isHorizontalScrolling ? `${Math.max(2000, tableColumns.length * 250)}px` : true }}
/>
{this.shouldShowPagination() && (
<div className={cx('pagination')}>
@ -503,7 +558,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
renderId(record: AlertType) {
return (
<TextEllipsisTooltip placement="top" content={`#${record.inside_organization_number}`}>
<Text type="secondary" className={cx(TEXT_ELLIPSIS_CLASS)}>
<Text type="secondary" className={cx(TEXT_ELLIPSIS_CLASS, 'overflow-child--line-1')}>
#{record.inside_organization_number}
</Text>
</TextEllipsisTooltip>
@ -518,7 +573,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
return (
<div>
<TextEllipsisTooltip placement="top" content={record.render_for_web.title}>
<Text type="link" size="medium" className={cx('overflow-parent')}>
<Text type="link" size="medium" className={cx('overflow-parent')} data-testid="integration-url">
<PluginLink
query={{
page: 'alert-groups',
@ -555,7 +610,11 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
content={record?.alert_receive_channel?.verbal_name || ''}
>
<IntegrationLogo integration={integration} scale={0.1} />
<Emoji className={cx(TEXT_ELLIPSIS_CLASS)} text={record.alert_receive_channel?.verbal_name || ''} />
<Emoji
className={cx(TEXT_ELLIPSIS_CLASS)}
text={record.alert_receive_channel?.verbal_name || ''}
data-testid="integration-name"
/>
</TextEllipsisTooltip>
);
};
@ -574,16 +633,60 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
);
};
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 (
<Text type="secondary">
{date} {time}
</Text>
);
}
return (
<VerticalGroup spacing="none">
<Text type="secondary">{m.format('MMM DD, YYYY')}</Text>
<Text type="secondary">{m.format('HH:mm')}</Text>
<Text type="secondary">{date}</Text>
<Text type="secondary">{time}</Text>
</VerticalGroup>
);
}
};
renderLabels = (item: AlertType) => {
if (!item.labels.length) {
return null;
}
return (
<TooltipBadge
borderType="secondary"
icon="tag-alt"
addPadding
text={item.labels?.length}
tooltipContent={
<VerticalGroup spacing="sm">
{item.labels.map((label) => (
<HorizontalGroup spacing="sm" key={label.key.id}>
<LabelTag label={label.key.name} value={label.value.name} key={label.key.id} />
<Button
size="sm"
icon="filter"
tooltip="Apply filter"
variant="secondary"
onClick={this.getApplyLabelFilterClickHandler(label)}
/>
</HorizontalGroup>
))}
</VerticalGroup>
}
/>
);
};
renderTeam(record: AlertType, teams: any) {
return (
@ -593,6 +696,41 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
);
}
getApplyLabelFilterClickHandler = (label: LabelKeyValue) => {
const {
store: { filtersStore },
} = this.props;
return () => {
const {
filters: { label: oldLabelFilter = [] },
} = this.state;
const labelToAddString = `${label.key.id}:${label.value.id}`;
if (oldLabelFilter.some((label) => label === labelToAddString)) {
return;
}
const newLabelFilter = [...oldLabelFilter, labelToAddString];
LocationHelper.update({ label: newLabelFilter }, 'partial');
filtersStore.setNeedToParseFilters(true);
};
};
renderCustomColumn = (column: AlertGroupColumn, alert: AlertType) => {
const matchingLabel = alert.labels?.find((label) => label.key.name === column.name)?.value.name;
return (
<TextEllipsisTooltip placement="top" content={matchingLabel}>
<Text type="secondary" className={cx(TEXT_ELLIPSIS_CLASS, 'overflow-child--line-1')}>
{matchingLabel}
</Text>
</TextEllipsisTooltip>
);
};
shouldShowPagination() {
const { alertGroupStore } = this.props.store;
@ -609,78 +747,90 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
});
};
getTableColumns(): Array<{ width: string; title: string; key: string; render }> {
const {
store: { filtersStore, grafanaTeamStore, hasFeature },
} = this.props;
getTableColumns(): TableColumn[] {
const { store } = this.props;
const { isHorizontalScrolling } = this.state;
const columns = [
{
width: '140px',
title: 'Status',
key: 'time',
render: this.renderStatus,
},
{
width: '10%',
const columnMapping: { [key: string]: TableColumn } = {
ID: {
title: 'ID',
key: 'id',
render: this.renderId,
width: isHorizontalScrolling ? '100px' : '10%',
},
{
width: '35%',
title: 'Title',
key: 'title',
render: this.renderTitle,
Status: {
title: 'Status',
key: 'time',
render: this.renderStatus,
width: '140px',
},
{
width: '5%',
Alerts: {
title: 'Alerts',
key: 'alerts',
render: this.renderAlertsCounter,
width: '100px',
},
{
width: '15%',
Integration: {
title: 'Integration',
key: 'source',
key: 'integration',
render: this.renderSource,
width: isHorizontalScrolling ? undefined : '15%',
},
{
width: '10%',
Title: {
title: 'Title',
key: 'title',
render: this.renderTitle,
width: isHorizontalScrolling ? undefined : '35%',
},
Created: {
title: 'Created',
key: 'created',
render: this.renderStartedAt,
width: isHorizontalScrolling ? undefined : '10%',
},
{
width: '10%',
Team: {
title: 'Team',
key: 'team',
render: (item: AlertType) => this.renderTeam(item, grafanaTeamStore.items),
render: (item: AlertType) => this.renderTeam(item, store.grafanaTeamStore.items),
width: isHorizontalScrolling ? undefined : '10%',
},
{
width: '15%',
Users: {
title: 'Users',
key: 'users',
render: renderRelatedUsers,
width: isHorizontalScrolling ? undefined : '15%',
},
];
};
if (hasFeature(AppFeature.Labels)) {
columns.splice(-2, 0, {
width: '5%',
if (store.hasFeature(AppFeature.Labels)) {
// add labels specific column if enabled
columnMapping['Labels'] = {
width: '60px',
title: 'Labels',
key: 'labels',
render: ({ labels }: AlertType) => (
<LabelsTooltipBadge
labels={labels}
onClick={(label) => filtersStore.applyLabelFilter(label, PAGE.Incidents)}
/>
),
});
columns.find((column) => column.key === 'title').width = '30%';
render: this.renderLabels,
};
} else {
// no filtering needed if we don't have Labels enabled
return Object.keys(columnMapping).map((col) => columnMapping[col]);
}
return columns;
const mappedColumns: TableColumn[] = store.alertGroupStore.columns
.filter((col) => col.isVisible)
.map((column: AlertGroupColumn): TableColumn => {
if (column.type === AlertGroupColumnType.DEFAULT && columnMapping[column.name]) {
return columnMapping[column.name];
}
return {
width: isHorizontalScrolling ? '200px' : '10%',
title: capitalize(column.name),
key: column.id,
render: (item: AlertType) => this.renderCustomColumn(column, item),
};
});
return mappedColumns;
}
getOnActionButtonClick = (incidentId: string, action: AlertAction): ((e: SyntheticEvent) => Promise<void>) => {

View file

@ -181,7 +181,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
<div className={cx('integration__heading-container')}>
<PluginLink query={{ page: 'integrations', ...query }} className={cx('back-arrow')}>
<IconButton name="arrow-left" size="xl" />
<IconButton aria-label="Go Back" name="arrow-left" size="xl" />
</PluginLink>
<h2 className={cx('integration__name')}>
<Emoji text={alertReceiveChannel.verbal_name} />

View file

@ -185,7 +185,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
<HorizontalGroup justify="space-between">
<div className={cx('title')}>
<PluginLink query={{ page: 'schedules', ...query }}>
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xl" />
<IconButton className="button-back" aria-label="Go Back" name="arrow-left" size="xl" />
</PluginLink>
<Text.Title
editable={false}

View file

@ -35,3 +35,7 @@
flex-grow: 1;
gap: 8px;
}
.button-back {
margin-top: 5px;
}

View file

@ -68,33 +68,33 @@ exports[`PluginSetup there is an error message - retry setup 1`] = `
class="configure-plugin"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-1pq88ji-button"
class="css-72lnkn-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Retry
</span>
</button>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<a
class="css-1pq88ji-button"
class="css-72lnkn-button"
href="/plugins/grafana-oncall-app?page=configuration"
tabindex="0"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Configure Plugin
</span>
@ -124,33 +124,33 @@ exports[`PluginSetup there is an error message 1`] = `
class="configure-plugin"
>
<div
class="css-ve64a7-horizontal-group"
class="css-ffyaiw-horizontal-group"
style="width: 100%; height: 100%;"
>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<button
class="css-1pq88ji-button"
class="css-72lnkn-button"
type="button"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Retry
</span>
</button>
</div>
<div
class="css-cvef6c-layoutChildrenWrapper"
class="css-18qv8yz-layoutChildrenWrapper"
>
<a
class="css-1pq88ji-button"
class="css-72lnkn-button"
href="/plugins/grafana-oncall-app?page=configuration"
tabindex="0"
>
<span
class="css-1mhnkuh"
class="css-1riaxdn"
>
Configure Plugin
</span>

View file

@ -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

View file

@ -6,6 +6,5 @@ import { RootStore } from './index';
export function useStore(): RootStore {
const { store } = React.useContext(MobXProviderContext);
return store;
}

View file

@ -54,3 +54,5 @@ export enum PAGE {
}
export const TEXT_ELLIPSIS_CLASS = 'overflow-child';
export const INCIDENT_HORIZONTAL_SCROLLING_STORAGE = 'isIncidentalTableHorizontalScrolling';

View file

@ -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<void> {
return async (...params) => {
LoaderStore.setLoadingAction(actionKey, true);
try {
await callback(...params);
} finally {
LoaderStore.setLoadingAction(actionKey, false);
}
};
}
type GlobalNotificationConfig = {
success?: string;
failure?: string;

View file

@ -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`;
}

View file

@ -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<any>;
}

View file

@ -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"