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 <maxim.mordasov@grafana.com>
This commit is contained in:
parent
bb8b88f720
commit
b797672626
20 changed files with 430 additions and 39 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ interface TooltipBadgeProps {
|
|||
className?: string;
|
||||
borderType: Partial<TextType>;
|
||||
text?: number | string;
|
||||
tooltipTitle: string;
|
||||
tooltipContent: React.ReactNode;
|
||||
|
||||
tooltipTitle?: string;
|
||||
icon?: IconName;
|
||||
customIcon?: React.ReactNode;
|
||||
addPadding?: boolean;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
.labels-list {
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
|
||||
> li {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
|
@ -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<HTMLInputElement>) => {
|
||||
setAlertGroupLabels((alertGroupLabels) => ({
|
||||
...alertGroupLabels,
|
||||
inheritable: { ...alertGroupLabels.inheritable, [keyId]: event.target.checked },
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer scrollableContent title="Alert group labels" onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup spacing="xs" align="flex-start">
|
||||
<Label>Inherited labels</Label>
|
||||
<Tooltip content="Labels inherited from integration">
|
||||
<Icon name="info-circle" className={cx('extra-fields__icon')} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
<ul className={cx('labels-list')}>
|
||||
{alertReceiveChannel.labels.length ? (
|
||||
alertReceiveChannel.labels.map((label) => (
|
||||
<li key={label.key.id}>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Input width={38} value={label.key.name} disabled />
|
||||
<Input width={31} value={label.value.name} disabled />
|
||||
<InlineSwitch
|
||||
value={alertGroupLabels.inheritable[label.key.id]}
|
||||
transparent
|
||||
onChange={getInheritanceChangeHandler(label.key.id)}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<VerticalGroup>
|
||||
<Text type="secondary">There are no labels to inherit yet</Text>
|
||||
<Text type="link" onClick={handleOpenIntegrationSettings} clickable>
|
||||
Add labels to the integration
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</ul>
|
||||
<div className={cx('buttons')}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default IntegrationLabelsForm;
|
||||
|
|
@ -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<string, any>, 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<string, any>;
|
||||
hadInteraction: boolean;
|
||||
lastRequestId: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,7 +301,6 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
<TooltipBadge
|
||||
borderType="primary"
|
||||
text="Payload"
|
||||
tooltipTitle=""
|
||||
tooltipContent=""
|
||||
className={cx('alert-groups-last-payload-badge')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export interface AlertReceiveChannel {
|
|||
allow_delete: boolean;
|
||||
deleted?: boolean;
|
||||
labels: LabelKeyValue[];
|
||||
alert_group_labels: { inheritable: Record<LabelKeyValue['key']['id'], boolean> };
|
||||
}
|
||||
|
||||
export interface AlertReceiveChannelChoice {
|
||||
|
|
|
|||
|
|
@ -30,9 +30,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
@observable
|
||||
alertGroupsLoading = false;
|
||||
|
||||
@observable
|
||||
needToParseFilters = false;
|
||||
|
||||
@observable
|
||||
incidentFilters: any;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IncidentPageProps, IncidentPageState>
|
|||
/>
|
||||
</div>
|
||||
|
||||
{Boolean(store.hasFeature(AppFeature.Labels) && incident.labels.length) && (
|
||||
<TooltipBadge
|
||||
borderType="secondary"
|
||||
icon="tag-alt"
|
||||
addPadding
|
||||
text={incident.labels.length}
|
||||
tooltipContent={
|
||||
<VerticalGroup spacing="sm">
|
||||
{incident.labels.map((label) => (
|
||||
<LabelTag label={label.key.name} value={label.value.name} key={label.key.id} />
|
||||
))}
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{integration && (
|
||||
<HorizontalGroup>
|
||||
<PluginLink
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { SyntheticEvent } from 'react';
|
||||
|
||||
import { LabelTag } from '@grafana/labels';
|
||||
import { Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -15,6 +16,7 @@ import ManualAlertGroup from 'components/ManualAlertGroup/ManualAlertGroup';
|
|||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
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 { IncidentsFiltersType } from 'containers/IncidentsFilters/IncidentFilters.types';
|
||||
|
|
@ -22,7 +24,9 @@ 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 { 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';
|
||||
|
|
@ -44,7 +48,7 @@ interface IncidentsPageProps extends WithStoreProps, PageProps, RouteComponentPr
|
|||
interface IncidentsPageState {
|
||||
selectedIncidentIds: Array<Alert['pk']>;
|
||||
affectedRows: { [key: string]: boolean };
|
||||
filters?: IncidentsFiltersType;
|
||||
filters?: Record<string, any>;
|
||||
pagination: Pagination;
|
||||
showAddAlertGroupForm: boolean;
|
||||
}
|
||||
|
|
@ -583,6 +587,37 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<TextEllipsisTooltip placement="top" content={teams[record.team]?.name}>
|
||||
|
|
@ -591,6 +626,29 @@ 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);
|
||||
};
|
||||
};
|
||||
|
||||
shouldShowPagination() {
|
||||
const { alertGroupStore } = this.props.store;
|
||||
|
||||
|
|
@ -610,7 +668,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
getTableColumns(): Array<{ width: string; title: string; key: string; render }> {
|
||||
const { store } = this.props;
|
||||
|
||||
return [
|
||||
const columns = [
|
||||
{
|
||||
width: '140px',
|
||||
title: 'Status',
|
||||
|
|
@ -660,6 +718,18 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
render: renderRelatedUsers,
|
||||
},
|
||||
];
|
||||
|
||||
if (store.hasFeature(AppFeature.Labels)) {
|
||||
columns.splice(-2, 0, {
|
||||
width: '5%',
|
||||
title: 'Labels',
|
||||
key: 'labels',
|
||||
render: (item: AlertType) => this.renderLabels(item),
|
||||
});
|
||||
columns.find((column) => column.key === 'title').width = '30%';
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
getOnActionButtonClick = (incidentId: string, action: AlertAction): ((e: SyntheticEvent) => Promise<void>) => {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import ExpandedIntegrationRouteDisplay from 'containers/IntegrationContainers/Ex
|
|||
import IntegrationHeartbeatForm from 'containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm';
|
||||
import IntegrationTemplateList from 'containers/IntegrationContainers/IntegrationTemplatesList';
|
||||
import IntegrationForm from 'containers/IntegrationForm/IntegrationForm';
|
||||
import IntegrationLabelsForm from 'containers/IntegrationLabelsForm/IntegrationLabelsForm';
|
||||
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
|
||||
import MaintenanceForm from 'containers/MaintenanceForm/MaintenanceForm';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
|
|
@ -731,7 +732,8 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
isLegacyIntegration,
|
||||
changeIsTemplateSettingsOpen,
|
||||
}) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const store = useStore();
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
|
|
@ -747,6 +749,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
}>(undefined);
|
||||
|
||||
const [isIntegrationSettingsOpen, setIsIntegrationSettingsOpen] = useState(false);
|
||||
const [labelsFormOpen, setLabelsFormOpen] = useState(false);
|
||||
const [isHeartbeatFormOpen, setIsHeartbeatFormOpen] = useState(false);
|
||||
const [isDemoModalOpen, setIsDemoModalOpen] = useState(false);
|
||||
const [maintenanceData, setMaintenanceData] = useState<{
|
||||
|
|
@ -789,6 +792,19 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{labelsFormOpen && (
|
||||
<IntegrationLabelsForm
|
||||
onHide={() => {
|
||||
setLabelsFormOpen(false);
|
||||
}}
|
||||
onSubmit={() => alertReceiveChannelStore.updateItem(alertReceiveChannel['id'])}
|
||||
id={alertReceiveChannel['id']}
|
||||
onOpenIntegraionSettings={() => {
|
||||
setIsIntegrationSettingsOpen(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isHeartbeatFormOpen && (
|
||||
<IntegrationHeartbeatForm
|
||||
alertReceveChannelId={alertReceiveChannel['id']}
|
||||
|
|
@ -826,6 +842,14 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
<Text type="primary">Integration Settings</Text>
|
||||
</div>
|
||||
|
||||
{store.hasFeature(AppFeature.Labels) && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integration__actionItem')} onClick={() => openLabelsForm()}>
|
||||
<Text type="primary">Alert group labels</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
{showHeartbeatSettings() && (
|
||||
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
|
||||
<div
|
||||
|
|
@ -1015,6 +1039,10 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
setIsIntegrationSettingsOpen(true);
|
||||
}
|
||||
|
||||
function openLabelsForm() {
|
||||
setLabelsFormOpen(true);
|
||||
}
|
||||
|
||||
function openStartMaintenance() {
|
||||
setMaintenanceData({ disabled: true, alert_receive_channel_id: alertReceiveChannel.id });
|
||||
}
|
||||
|
|
@ -1061,20 +1089,17 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
|||
</PluginLink>
|
||||
)}
|
||||
|
||||
{renderLabels && (
|
||||
{Boolean(renderLabels && alertReceiveChannel.labels.length) && (
|
||||
<TooltipBadge
|
||||
tooltipTitle=""
|
||||
borderType="secondary"
|
||||
icon="tag-alt"
|
||||
addPadding
|
||||
text={alertReceiveChannel.labels.length}
|
||||
tooltipContent={
|
||||
<VerticalGroup spacing="sm">
|
||||
{alertReceiveChannel.labels.length
|
||||
? alertReceiveChannel.labels.map((label) => (
|
||||
<LabelTag label={label.key.name} value={label.value.name} key={label.key.id} />
|
||||
))
|
||||
: 'No labels attached'}
|
||||
{alertReceiveChannel.labels.map((label) => (
|
||||
<LabelTag label={label.key.name} value={label.value.name} key={label.key.id} />
|
||||
))}
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTool
|
|||
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
|
||||
import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu';
|
||||
import IntegrationForm from 'containers/IntegrationForm/IntegrationForm';
|
||||
import IntegrationLabelsForm from 'containers/IntegrationLabelsForm/IntegrationLabelsForm';
|
||||
import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
|
|
@ -80,6 +81,7 @@ const FILTERS_DEBOUNCE_MS = 500;
|
|||
interface IntegrationsState extends PageBaseState {
|
||||
integrationsFilters: SupportedIntegrationFilters;
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'] | 'new';
|
||||
alertReceiveChannelIdToShowLabels?: AlertReceiveChannel['id'];
|
||||
confirmationModal: {
|
||||
isOpen: boolean;
|
||||
title: any;
|
||||
|
|
@ -192,7 +194,13 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
|
||||
render() {
|
||||
const { store, query } = this.props;
|
||||
const { alertReceiveChannelId, confirmationModal, activeTab, integrationsFilters } = this.state;
|
||||
const {
|
||||
alertReceiveChannelId,
|
||||
alertReceiveChannelIdToShowLabels,
|
||||
confirmationModal,
|
||||
activeTab,
|
||||
integrationsFilters,
|
||||
} = this.state;
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
const { count, results, page_size } = alertReceiveChannelStore.getPaginatedSearchResult();
|
||||
|
|
@ -289,6 +297,19 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
/>
|
||||
)}
|
||||
|
||||
{alertReceiveChannelIdToShowLabels && (
|
||||
<IntegrationLabelsForm
|
||||
onHide={() => {
|
||||
this.setState({ alertReceiveChannelIdToShowLabels: undefined });
|
||||
}}
|
||||
onSubmit={this.update}
|
||||
id={alertReceiveChannelIdToShowLabels}
|
||||
onOpenIntegraionSettings={(id: AlertReceiveChannel['id']) => {
|
||||
this.setState({ alertReceiveChannelId: id });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{confirmationModal && (
|
||||
<ConfirmModal
|
||||
isOpen={confirmationModal.isOpen}
|
||||
|
|
@ -369,7 +390,6 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
borderType="primary"
|
||||
placement="top"
|
||||
text={alertReceiveChannelCounter?.alerts_count + '/' + alertReceiveChannelCounter?.alert_groups_count}
|
||||
tooltipTitle=""
|
||||
tooltipContent={
|
||||
alertReceiveChannelCounter?.alerts_count +
|
||||
' alert' +
|
||||
|
|
@ -452,29 +472,30 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
}
|
||||
|
||||
renderLabels(item: AlertReceiveChannel) {
|
||||
if (!item.labels.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipBadge
|
||||
tooltipTitle=""
|
||||
borderType="secondary"
|
||||
icon="tag-alt"
|
||||
addPadding
|
||||
text={item.labels?.length}
|
||||
tooltipContent={
|
||||
<VerticalGroup spacing="sm">
|
||||
{item.labels?.length
|
||||
? 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>
|
||||
))
|
||||
: 'No labels attached'}
|
||||
{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>
|
||||
}
|
||||
/>
|
||||
|
|
@ -490,6 +511,8 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
}
|
||||
|
||||
renderButtons = (item: AlertReceiveChannel) => {
|
||||
const { store } = this.props;
|
||||
|
||||
return (
|
||||
<WithContextMenu
|
||||
renderMenuItems={() => (
|
||||
|
|
@ -500,6 +523,14 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
{store.hasFeature(AppFeature.Labels) && (
|
||||
<WithPermissionControlTooltip key="edit" userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integrations-actionItem')} onClick={() => this.onLabelsEditClick(item.id)}>
|
||||
<Text type="primary">Alert group labels</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
<CopyToClipboard text={item.id} onCopy={() => openNotification('Integration ID has been copied')}>
|
||||
<div className={cx('integrations-actionItem')}>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
|
|
@ -631,6 +662,10 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
this.setState({ alertReceiveChannelId: id });
|
||||
};
|
||||
|
||||
onLabelsEditClick = (id: AlertReceiveChannel['id']) => {
|
||||
this.setState({ alertReceiveChannelIdToShowLabels: id });
|
||||
};
|
||||
|
||||
handleDeleteAlertReceiveChannel = (alertReceiveChannelId: AlertReceiveChannel['id']) => {
|
||||
const { store } = this.props;
|
||||
|
||||
|
|
@ -666,7 +701,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
|
||||
LocationHelper.update({ label: newLabelFilter }, 'partial');
|
||||
|
||||
filtersStore.needToParseFilters = true;
|
||||
filtersStore.setNeedToParseFilters(true);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue