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:
Vadim Stepanov 2023-11-15 12:10:34 +00:00 committed by GitHub
parent bb8b88f720
commit b797672626
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 430 additions and 39 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,13 @@
.labels-list {
margin: 0;
list-style-type: none;
> li {
margin: 10px 0;
}
}
.buttons {
width: 100%;
margin-top: 30px;
}

View file

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

View file

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

View file

@ -301,7 +301,6 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
<TooltipBadge
borderType="primary"
text="Payload"
tooltipTitle=""
tooltipContent=""
className={cx('alert-groups-last-payload-badge')}
/>

View file

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

View file

@ -30,9 +30,6 @@ export class AlertGroupStore extends BaseStore {
@observable
alertGroupsLoading = false;
@observable
needToParseFilters = false;
@observable
incidentFilters: any;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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