From bb8b88f720b22e6a5419b242afe1b49d46cfbb42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 18:38:48 -0500 Subject: [PATCH 01/12] Bump aiohttp from 3.8.5 to 3.8.6 in /dev/scripts/generate-fake-data (#3354) --- dev/scripts/generate-fake-data/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/scripts/generate-fake-data/requirements.txt b/dev/scripts/generate-fake-data/requirements.txt index 976c00d2..2b67a670 100644 --- a/dev/scripts/generate-fake-data/requirements.txt +++ b/dev/scripts/generate-fake-data/requirements.txt @@ -1,3 +1,3 @@ -aiohttp==3.8.5 +aiohttp==3.8.6 Faker==16.4.0 tqdm==4.64.1 From b797672626af82babe356c31d2b59956ef566bc1 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Wed, 15 Nov 2023 12:10:34 +0000 Subject: [PATCH 02/12] Inheritable integration labels (#3307) # What this PR does Allows selecting integration labels that will be passed down to alert groups ## Which issue(s) this PR fixes Related to https://github.com/grafana/oncall-private/issues/2179 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --------- Co-authored-by: Maxim --- .../alerts/models/alert_receive_channel.py | 19 ++++ .../api/serializers/alert_receive_channel.py | 14 +++ .../api/tests/test_alert_receive_channel.py | 66 +++++++++++ ...rtreceivechannelassociatedlabel_inherit.py | 18 +++ engine/apps/labels/models.py | 3 + engine/apps/labels/tests/test_alert_group.py | 1 + engine/apps/labels/utils.py | 5 +- .../components/TooltipBadge/TooltipBadge.tsx | 2 +- .../IntegrationLabelsForm.module.css | 13 +++ .../IntegrationLabelsForm.tsx | 105 ++++++++++++++++++ .../RemoteFilters/RemoteFilters.tsx | 4 +- .../TemplatesAlertGroupsList.tsx | 1 - .../alert_receive_channel.types.ts | 1 + .../src/models/alertgroup/alertgroup.ts | 3 - .../src/models/alertgroup/alertgroup.types.ts | 2 + grafana-plugin/src/models/filters/filters.ts | 7 +- .../src/pages/incident/Incident.tsx | 19 ++++ .../src/pages/incidents/Incidents.tsx | 74 +++++++++++- .../src/pages/integration/Integration.tsx | 41 +++++-- .../src/pages/integrations/Integrations.tsx | 71 +++++++++--- 20 files changed, 430 insertions(+), 39 deletions(-) create mode 100644 engine/apps/labels/migrations/0003_alertreceivechannelassociatedlabel_inherit.py create mode 100644 grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.module.css create mode 100644 grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 826b1b96..73b7f0ed 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -87,6 +87,10 @@ def number_to_smiles_translator(number): return "".join(reversed(smileset)) +class IntegrationAlertGroupLabels(typing.TypedDict): + inheritable: typing.Dict[str, bool] + + class AlertReceiveChannelQueryset(models.QuerySet): def delete(self): self.update(deleted_at=timezone.now()) @@ -638,6 +642,21 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): result["team"] = "General" return result + @property + def alert_group_labels(self) -> IntegrationAlertGroupLabels: + """ + Alert group labels configuration for the integration used by AlertReceiveChannelSerializer. + See AlertReceiveChannelAssociatedLabel.inheritable for more details. + """ + return {"inheritable": {label.key_id: label.inheritable for label in self.labels.all()}} + + @alert_group_labels.setter + def alert_group_labels(self, value: IntegrationAlertGroupLabels) -> None: + """Setter for alert_group_labels used by AlertReceiveChannelSerializer""" + inheritable_key_ids = [key_id for key_id, inheritable in value["inheritable"].items() if inheritable] + self.labels.filter(key_id__in=inheritable_key_ids).update(inheritable=True) + self.labels.filter(~Q(key_id__in=inheritable_key_ids)).update(inheritable=False) + @receiver(post_save, sender=AlertReceiveChannel) def listen_for_alertreceivechannel_model_save( diff --git a/engine/apps/api/serializers/alert_receive_channel.py b/engine/apps/api/serializers/alert_receive_channel.py index 07d4be8c..0ab5171f 100644 --- a/engine/apps/api/serializers/alert_receive_channel.py +++ b/engine/apps/api/serializers/alert_receive_channel.py @@ -33,6 +33,12 @@ def valid_jinja_template_for_serializer_method_field(template): pass +class IntegrationAlertGroupLabelsSerializer(serializers.Serializer): + """Alert group labels configuration for the integration. See AlertReceiveChannel.alert_group_labels for details.""" + + inheritable = serializers.DictField(child=serializers.BooleanField()) + + class AlertReceiveChannelSerializer(EagerLoadingMixin, LabelsSerializerMixin, serializers.ModelSerializer): id = serializers.CharField(read_only=True, source="public_primary_key") integration_url = serializers.ReadOnlyField() @@ -55,6 +61,7 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, LabelsSerializerMixin, se connected_escalations_chains_count = serializers.SerializerMethodField() inbound_email = serializers.CharField(required=False) is_legacy = serializers.SerializerMethodField() + alert_group_labels = IntegrationAlertGroupLabelsSerializer(required=False) # integration heartbeat is in PREFETCH_RELATED not by mistake. # With using of select_related ORM builds strange join @@ -95,6 +102,7 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, LabelsSerializerMixin, se "inbound_email", "is_legacy", "labels", + "alert_group_labels", ] read_only_fields = [ "created_at", @@ -128,6 +136,7 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, LabelsSerializerMixin, se is_able_to_autoresolve = _integration.is_able_to_autoresolve labels = validated_data.pop("labels", None) + alert_group_labels = validated_data.pop("alert_group_labels", None) try: instance = AlertReceiveChannel.create( **validated_data, @@ -137,7 +146,12 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, LabelsSerializerMixin, se ) except AlertReceiveChannel.DuplicateDirectPagingError: raise BadRequest(detail=AlertReceiveChannel.DuplicateDirectPagingError.DETAIL) + + # Create label associations first, then update inheritable labels self.update_labels_association_if_needed(labels, instance, organization) + if alert_group_labels: + instance.alert_group_labels = alert_group_labels + return instance def update(self, instance, validated_data): diff --git a/engine/apps/api/tests/test_alert_receive_channel.py b/engine/apps/api/tests/test_alert_receive_channel.py index 194a5645..4f0b5a49 100644 --- a/engine/apps/api/tests/test_alert_receive_channel.py +++ b/engine/apps/api/tests/test_alert_receive_channel.py @@ -1350,3 +1350,69 @@ def test_update_alert_receive_channel_labels_duplicate_key( assert response.status_code == status.HTTP_400_BAD_REQUEST assert alert_receive_channel.labels.count() == 0 + + +@pytest.mark.django_db +def test_alert_group_labels_get( + make_organization_and_user_with_plugin_token, + make_alert_receive_channel, + make_integration_label_association, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token() + alert_receive_channel = make_alert_receive_channel(organization) + + client = APIClient() + url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": alert_receive_channel.public_primary_key}) + + response = client.get(url, **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert response.json()["alert_group_labels"] == {"inheritable": {}} + + label = make_integration_label_association(organization, alert_receive_channel) + response = client.get(url, **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert response.json()["alert_group_labels"] == {"inheritable": {label.key_id: True}} + + +@pytest.mark.django_db +def test_alert_group_labels_put( + make_organization_and_user_with_plugin_token, + make_alert_receive_channel, + make_integration_label_association, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token() + alert_receive_channel = make_alert_receive_channel(organization) + label_1 = make_integration_label_association(organization, alert_receive_channel) + label_2 = make_integration_label_association(organization, alert_receive_channel, inheritable=False) + + client = APIClient() + url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": alert_receive_channel.public_primary_key}) + data = {"alert_group_labels": {"inheritable": {label_1.key_id: False, label_2.key_id: True}}} + response = client.put(url, data, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_200_OK + assert response.json()["alert_group_labels"] == {"inheritable": {label_1.key_id: False, label_2.key_id: True}} + + +@pytest.mark.django_db +def test_alert_group_labels_post(alert_receive_channel_internal_api_setup, make_user_auth_headers): + user, token, _ = alert_receive_channel_internal_api_setup + + labels = [{"key": {"id": "test", "name": "test"}, "value": {"id": "123", "name": "123"}}] + alert_group_labels = {"inheritable": {"test": False}} + data = { + "integration": AlertReceiveChannel.INTEGRATION_GRAFANA, + "team": None, + "labels": labels, + "alert_group_labels": alert_group_labels, + } + + client = APIClient() + url = reverse("api-internal:alert_receive_channel-list") + response = client.post(url, data, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["labels"] == labels + assert response.json()["alert_group_labels"] == alert_group_labels diff --git a/engine/apps/labels/migrations/0003_alertreceivechannelassociatedlabel_inherit.py b/engine/apps/labels/migrations/0003_alertreceivechannelassociatedlabel_inherit.py new file mode 100644 index 00000000..fcba6e8a --- /dev/null +++ b/engine/apps/labels/migrations/0003_alertreceivechannelassociatedlabel_inherit.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-11-09 10:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('labels', '0002_alertgroupassociatedlabel_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='alertreceivechannelassociatedlabel', + name='inheritable', + field=models.BooleanField(default=True, null=True), + ), + ] diff --git a/engine/apps/labels/models.py b/engine/apps/labels/models.py index 233d47b4..f9861a72 100644 --- a/engine/apps/labels/models.py +++ b/engine/apps/labels/models.py @@ -105,6 +105,9 @@ class AlertReceiveChannelAssociatedLabel(AssociatedLabel): "alerts.AlertReceiveChannel", on_delete=models.CASCADE, related_name="labels" ) + # If inheritable is True, then the label will be passed down to alert groups + inheritable = models.BooleanField(default=True, null=True) + class Meta: unique_together = ["key_id", "value_id", "alert_receive_channel_id"] diff --git a/engine/apps/labels/tests/test_alert_group.py b/engine/apps/labels/tests/test_alert_group.py index d022926c..a5ac35ca 100644 --- a/engine/apps/labels/tests/test_alert_group.py +++ b/engine/apps/labels/tests/test_alert_group.py @@ -32,6 +32,7 @@ def test_assign_labels(make_organization, make_alert_receive_channel, make_integ organization = make_organization() alert_receive_channel = make_alert_receive_channel(organization) label = make_integration_label_association(organization, alert_receive_channel) + make_integration_label_association(organization, alert_receive_channel, inheritable=False) alert = Alert.create( title="the title", diff --git a/engine/apps/labels/utils.py b/engine/apps/labels/utils.py index b26e6c63..98d1bf95 100644 --- a/engine/apps/labels/utils.py +++ b/engine/apps/labels/utils.py @@ -55,8 +55,7 @@ def assign_labels(alert_group: "AlertGroup", alert_receive_channel: "AlertReceiv if not is_labels_feature_enabled(alert_receive_channel.organization): return - # inherit all labels from the integration - # FIXME: this is a temporary solution before we have a UI for configuring inherited labels + # inherit labels from the integration alert_group_labels = [ AlertGroupAssociatedLabel( alert_group=alert_group, @@ -64,6 +63,6 @@ def assign_labels(alert_group: "AlertGroup", alert_receive_channel: "AlertReceiv key_name=label.key.name, value_name=label.value.name, ) - for label in alert_receive_channel.labels.all().select_related("key", "value") + for label in alert_receive_channel.labels.filter(inheritable=True).select_related("key", "value") ] AlertGroupAssociatedLabel.objects.bulk_create(alert_group_labels) diff --git a/grafana-plugin/src/components/TooltipBadge/TooltipBadge.tsx b/grafana-plugin/src/components/TooltipBadge/TooltipBadge.tsx index 2d36667b..99ede4a1 100644 --- a/grafana-plugin/src/components/TooltipBadge/TooltipBadge.tsx +++ b/grafana-plugin/src/components/TooltipBadge/TooltipBadge.tsx @@ -11,9 +11,9 @@ interface TooltipBadgeProps { className?: string; borderType: Partial; text?: number | string; - tooltipTitle: string; tooltipContent: React.ReactNode; + tooltipTitle?: string; icon?: IconName; customIcon?: React.ReactNode; addPadding?: boolean; diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.module.css b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.module.css new file mode 100644 index 00000000..89d574a5 --- /dev/null +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.module.css @@ -0,0 +1,13 @@ +.labels-list { + margin: 0; + list-style-type: none; + + > li { + margin: 10px 0; + } +} + +.buttons { + width: 100%; + margin-top: 30px; +} diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx new file mode 100644 index 00000000..4577a293 --- /dev/null +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; + +import { Button, Drawer, HorizontalGroup, Icon, InlineSwitch, Input, Label, Tooltip, VerticalGroup } from '@grafana/ui'; +import cn from 'classnames/bind'; +import { observer } from 'mobx-react'; + +import Text from 'components/Text/Text'; +import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { LabelKey } from 'models/label/label.types'; +import { useStore } from 'state/useStore'; + +import styles from './IntegrationLabelsForm.module.css'; + +const cx = cn.bind(styles); + +interface IntegrationLabelsFormProps { + id: AlertReceiveChannel['id']; + onSubmit: () => void; + onHide: () => void; + onOpenIntegraionSettings: (id: AlertReceiveChannel['id']) => void; +} + +const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => { + const { id, onHide, onSubmit, onOpenIntegraionSettings } = props; + + const store = useStore(); + + const { alertReceiveChannelStore } = store; + + const alertReceiveChannel = alertReceiveChannelStore.items[id]; + + const [alertGroupLabels, setAlertGroupLabels] = useState(alertReceiveChannel.alert_group_labels); + + const handleSave = () => { + alertReceiveChannelStore.saveAlertReceiveChannel(id, { alert_group_labels: alertGroupLabels }); + + onSubmit(); + + onHide(); + }; + + const handleOpenIntegrationSettings = () => { + onHide(); + + onOpenIntegraionSettings(id); + }; + + const getInheritanceChangeHandler = (keyId: LabelKey['id']) => { + return (event: React.ChangeEvent) => { + setAlertGroupLabels((alertGroupLabels) => ({ + ...alertGroupLabels, + inheritable: { ...alertGroupLabels.inheritable, [keyId]: event.target.checked }, + })); + }; + }; + + return ( + + + + + + + + +
    + {alertReceiveChannel.labels.length ? ( + alertReceiveChannel.labels.map((label) => ( +
  • + + + + + +
  • + )) + ) : ( + + There are no labels to inherit yet + + Add labels to the integration + + + )} +
+
+ + + + +
+
+
+ ); +}); + +export default IntegrationLabelsForm; diff --git a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx index 13be43ab..f816c77a 100644 --- a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx +++ b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx @@ -38,7 +38,7 @@ import styles from './RemoteFilters.module.css'; const cx = cn.bind(styles); interface RemoteFiltersProps extends WithStoreProps { - onChange: (filters: { [key: string]: any }, isOnMount: boolean, invalidateFn: () => boolean) => void; + onChange: (filters: Record, isOnMount: boolean, invalidateFn: () => boolean) => void; query: KeyValue; page: PAGE; defaultFilters?: FiltersValues; @@ -49,7 +49,7 @@ interface RemoteFiltersProps extends WithStoreProps { interface RemoteFiltersState { filterOptions?: FilterOption[]; filters: FilterOption[]; - values: { [key: string]: any }; + values: Record; hadInteraction: boolean; lastRequestId: string; } diff --git a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx index 8c49e1dd..1b2127d7 100644 --- a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx +++ b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx @@ -301,7 +301,6 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts index ebb2fbba..ec366bf2 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts @@ -49,6 +49,7 @@ export interface AlertReceiveChannel { allow_delete: boolean; deleted?: boolean; labels: LabelKeyValue[]; + alert_group_labels: { inheritable: Record }; } export interface AlertReceiveChannelChoice { diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 80a8cb29..e2fecd3c 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -30,9 +30,6 @@ export class AlertGroupStore extends BaseStore { @observable alertGroupsLoading = false; - @observable - needToParseFilters = false; - @observable incidentFilters: any; diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index 2a707d73..082c6186 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -1,6 +1,7 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { Channel } from 'models/channel'; import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; +import { LabelKeyValue } from 'models/label/label.types'; import { PagedUser, User } from 'models/user/user.types'; export enum IncidentStatus { @@ -82,6 +83,7 @@ export interface Alert { paged_users: PagedUser[]; team: GrafanaTeam['id']; grafana_incident_id: string | null; + labels: LabelKeyValue[]; // set by client loading?: boolean; diff --git a/grafana-plugin/src/models/filters/filters.ts b/grafana-plugin/src/models/filters/filters.ts index 472dd727..ac4a63f3 100644 --- a/grafana-plugin/src/models/filters/filters.ts +++ b/grafana-plugin/src/models/filters/filters.ts @@ -24,7 +24,7 @@ export class FiltersStore extends BaseStore { private _globalValues: FiltersValues = {}; @observable - public needToParseFilters = false; + needToParseFilters = false; constructor(rootStore: RootStore) { super(rootStore); @@ -35,6 +35,11 @@ export class FiltersStore extends BaseStore { } } + @action + setNeedToParseFilters(value: boolean) { + this.needToParseFilters = value; + } + set globalValues(value: any) { this._globalValues = value; diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 64ac84ed..5a913c77 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -1,5 +1,6 @@ import React, { useState, SyntheticEvent } from 'react'; +import { LabelTag } from '@grafana/labels'; import { Button, HorizontalGroup, @@ -34,6 +35,7 @@ import { PluginBridge, SupportedPlugin } from 'components/PluginBridge/PluginBri import PluginLink from 'components/PluginLink/PluginLink'; import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; +import TooltipBadge from 'components/TooltipBadge/TooltipBadge'; import AddResponders from 'containers/AddResponders/AddResponders'; import { prepareForUpdate } from 'containers/AddResponders/AddResponders.helpers'; import { UserResponder } from 'containers/AddResponders/AddResponders.types'; @@ -50,6 +52,7 @@ import { import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types'; import { User } from 'models/user/user.types'; import { IncidentDropdown } from 'pages/incidents/parts/IncidentDropdown'; +import { AppFeature } from 'state/features'; import { PageProps, WithStoreProps } from 'state/types'; import { useStore } from 'state/useStore'; import { withMobXProviderContext } from 'state/withStore'; @@ -339,6 +342,22 @@ class IncidentPage extends React.Component /> + {Boolean(store.hasFeature(AppFeature.Labels) && incident.labels.length) && ( + + {incident.labels.map((label) => ( + + ))} + + } + /> + )} + {integration && ( ; affectedRows: { [key: string]: boolean }; - filters?: IncidentsFiltersType; + filters?: Record; pagination: Pagination; showAddAlertGroupForm: boolean; } @@ -583,6 +587,37 @@ class Incidents extends React.Component ); } + renderLabels(item: AlertType) { + if (!item.labels.length) { + return null; + } + + return ( + + {item.labels.map((label) => ( + + +