remove deprecated heartbeat_heartbeat table/model (#2534)

# What this PR does

- Remove `heartbeat_heartbeat` table. This model/table does not seems to
be deprecated/used anywhere (no data in this in production/staging; see
more comments in the code about this).
This commit is contained in:
Joey Orlando 2023-07-17 07:38:04 +02:00 committed by GitHub
parent aa4edad4a7
commit 63ac0972c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 111 additions and 324 deletions

View file

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Remove deprecated `heartbeat.HeartBeat` model/table by @joeyorlando ([#2534](https://github.com/grafana/oncall/pull/2534))
## v1.3.12 (2023-07-14)
### Added

View file

@ -303,7 +303,7 @@ To configure this feature as such:
1. Create a Webhook, or Formatted Webhook, Integration type.
1. Under the "Heartbeat" tab in the Integration modal, copy the unique heartbeat URL that is shown.
1. Set the hearbeat's expected time interval to 15 minutes (see note below regarding `ALERT_GROUP_ESCALATION_AUDITOR_CELERY_TASK_HEARTBEAT_INTERVAL`)
1. Set the heartbeat's expected time interval to 15 minutes (see note below regarding `ALERT_GROUP_ESCALATION_AUDITOR_CELERY_TASK_HEARTBEAT_INTERVAL`)
1. Configure the integration's escalation chain as necessary
1. Populate the following env variables:

View file

@ -1,5 +0,0 @@
from django.contrib import admin
from .models import HeartBeat
admin.site.register(HeartBeat)

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-07-14 11:36
from django.db import migrations
import django_migration_linter as linter
class Migration(migrations.Migration):
dependencies = [
('heartbeat', '0001_squashed_initial'),
]
operations = [
linter.IgnoreMigration(),
migrations.DeleteModel(
name='HeartBeat',
),
]

View file

@ -1,4 +1,5 @@
import logging
import typing
from urllib.parse import urljoin
from django.conf import settings
@ -26,13 +27,19 @@ def generate_public_primary_key_for_integration_heart_beat():
return new_public_primary_key
class BaseHeartBeat(models.Model):
"""
Implements base heartbeat logic
"""
class Meta:
abstract = True
class IntegrationHeartBeat(models.Model):
TIMEOUT_CHOICES = (
(60, "1 minute"),
(120, "2 minutes"),
(180, "3 minutes"),
(300, "5 minutes"),
(600, "10 minutes"),
(900, "15 minutes"),
(1800, "30 minutes"),
(3600, "1 hour"),
(43200, "12 hours"),
(86400, "1 day"),
)
created_at = models.DateTimeField(auto_now_add=True)
timeout_seconds = models.IntegerField(default=0)
@ -41,8 +48,43 @@ class BaseHeartBeat(models.Model):
actual_check_up_task_id = models.CharField(max_length=100)
previous_alerted_state_was_life = models.BooleanField(default=True)
public_primary_key = models.CharField(
max_length=20,
validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)],
unique=True,
default=generate_public_primary_key_for_integration_heart_beat,
)
alert_receive_channel = models.OneToOneField(
"alerts.AlertReceiveChannel", on_delete=models.CASCADE, related_name="integration_heartbeat"
)
@property
def is_expired(self) -> bool:
if self.last_heartbeat_time is None:
# else heartbeat flow was not received, so heartbeat can't expire.
return False
# if heartbeat signal was received check timeout
return self.last_heartbeat_time + timezone.timedelta(seconds=self.timeout_seconds) < timezone.now()
@property
def status(self) -> bool:
"""
Return bool indicates heartbeat status.
True if first heartbeat signal was sent and flow is ok else False.
If first heartbeat signal was not send it means that configuration was not finished and status not ok.
"""
if self.last_heartbeat_time is None:
return False
return not self.is_expired
@property
def link(self) -> str:
return urljoin(self.alert_receive_channel.integration_url, "heartbeat/")
@classmethod
def perform_heartbeat_check(cls, heartbeat_id, task_request_id):
def perform_heartbeat_check(cls, heartbeat_id: int, task_request_id: str) -> None:
with transaction.atomic():
heartbeats = cls.objects.filter(pk=heartbeat_id).select_for_update()
if len(heartbeats) == 0:
@ -54,7 +96,7 @@ class BaseHeartBeat(models.Model):
else:
logger.info(f"Heartbeat {heartbeat_id} is not actual {task_request_id}")
def check_heartbeat_state_and_save(self):
def check_heartbeat_state_and_save(self) -> bool:
"""
Use this method if you want just check heartbeat status.
"""
@ -63,7 +105,7 @@ class BaseHeartBeat(models.Model):
self.save(update_fields=["previous_alerted_state_was_life"])
return state_changed
def check_heartbeat_state(self):
def check_heartbeat_state(self) -> bool:
"""
Actually checking heartbeat.
Use this method if you want to do changes of heartbeat instance while checking its status.
@ -82,120 +124,7 @@ class BaseHeartBeat(models.Model):
state_changed = True
return state_changed
def on_heartbeat_restored(self):
raise NotImplementedError
def on_heartbeat_expired(self):
raise NotImplementedError
@property
def is_expired(self):
return self.last_heartbeat_time + timezone.timedelta(seconds=self.timeout_seconds) < timezone.now()
@property
def expiration_time(self):
return self.last_heartbeat_time + timezone.timedelta(seconds=self.timeout_seconds)
class HeartBeat(BaseHeartBeat):
"""
HeartBeat Integration itself
"""
alert_receive_channel = models.ForeignKey(
"alerts.AlertReceiveChannel", on_delete=models.CASCADE, related_name="heartbeats"
)
message = models.TextField(default="")
title = models.TextField(default="HeartBeat Title")
link = models.URLField(max_length=500, default=None, null=True)
user_defined_id = models.CharField(default="default", max_length=100)
def on_heartbeat_restored(self):
create_alert.apply_async(
kwargs={
"title": "[OK] " + self.title,
"message": self.title,
"image_url": None,
"link_to_upstream_details": self.link,
"alert_receive_channel_pk": self.alert_receive_channel.pk,
"integration_unique_data": {},
"raw_request_data": {
"is_resolve": True,
"id": self.pk,
"user_defined_id": self.user_defined_id,
},
},
)
def on_heartbeat_expired(self):
create_alert.apply_async(
kwargs={
"title": "[EXPIRED] " + self.title,
"message": self.message
+ "\nCreated: {}\nExpires: {}\nLast HeartBeat: {}".format(
self.created_at,
self.expiration_time,
self.last_checkup_task_time,
),
"image_url": None,
"link_to_upstream_details": self.link,
"alert_receive_channel_pk": self.alert_receive_channel.pk,
"integration_unique_data": {},
"raw_request_data": {
"is_resolve": False,
"id": self.pk,
"user_defined_id": self.user_defined_id,
},
}
)
class Meta:
unique_together = (("alert_receive_channel", "user_defined_id"),)
class IntegrationHeartBeat(BaseHeartBeat):
"""
HeartBeat for Integration (FormattedWebhook, Grafana, etc.)
"""
public_primary_key = models.CharField(
max_length=20,
validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)],
unique=True,
default=generate_public_primary_key_for_integration_heart_beat,
)
alert_receive_channel = models.OneToOneField(
"alerts.AlertReceiveChannel", on_delete=models.CASCADE, related_name="integration_heartbeat"
)
@property
def is_expired(self):
if self.last_heartbeat_time is not None:
# if heartbeat signal was received check timeout
return self.last_heartbeat_time + timezone.timedelta(seconds=self.timeout_seconds) < timezone.now()
else:
# else heartbeat flow was not received, so heartbeat can't expire.
return False
@property
def status(self):
"""
Return bool indicates heartbeat status.
True if first heartbeat signal was sent and flow is ok else False.
If first heartbeat signal was not send it means that configuration was not finished and status not ok.
"""
if self.last_heartbeat_time is not None:
return not self.is_expired
else:
return False
@property
def link(self):
return urljoin(self.alert_receive_channel.integration_url, "heartbeat/")
def on_heartbeat_restored(self):
def on_heartbeat_restored(self) -> None:
create_alert.apply_async(
kwargs={
"title": self.alert_receive_channel.heartbeat_restored_title,
@ -208,7 +137,7 @@ class IntegrationHeartBeat(BaseHeartBeat):
},
)
def on_heartbeat_expired(self):
def on_heartbeat_expired(self) -> None:
create_alert.apply_async(
kwargs={
"title": self.alert_receive_channel.heartbeat_expired_title,
@ -221,36 +150,23 @@ class IntegrationHeartBeat(BaseHeartBeat):
},
)
TIMEOUT_CHOICES = (
(60, "1 minute"),
(120, "2 minutes"),
(180, "3 minutes"),
(300, "5 minutes"),
(600, "10 minutes"),
(900, "15 minutes"),
(1800, "30 minutes"),
(3600, "1 hour"),
(43200, "12 hours"),
(86400, "1 day"),
)
# Insight logs
@property
def insight_logs_type_verbal(self):
def insight_logs_type_verbal(self) -> str:
return "integration_heartbeat"
@property
def insight_logs_verbal(self):
def insight_logs_verbal(self) -> str:
return f"Integration Heartbeat for {self.alert_receive_channel.insight_logs_verbal}"
@property
def insight_logs_serialized(self):
def insight_logs_serialized(self) -> typing.Dict[str, str | int]:
return {
"timeout": self.timeout_seconds,
}
@property
def insight_logs_metadata(self):
def insight_logs_metadata(self) -> typing.Dict[str, str]:
return {
"integration": self.alert_receive_channel.insight_logs_verbal,
"integration_id": self.alert_receive_channel.public_primary_key,

View file

@ -10,36 +10,12 @@ from common.custom_celery_tasks import shared_dedicated_queue_retry_task
logger = get_task_logger(__name__)
@shared_dedicated_queue_retry_task(bind=True)
def heartbeat_checkup(self, heartbeat_id):
HeartBeat = apps.get_model("heartbeat", "HeartBeat")
HeartBeat.perform_heartbeat_check(heartbeat_id, heartbeat_checkup.request.id)
@shared_dedicated_queue_retry_task()
def integration_heartbeat_checkup(heartbeat_id):
def integration_heartbeat_checkup(heartbeat_id: int) -> None:
IntegrationHeartBeat = apps.get_model("heartbeat", "IntegrationHeartBeat")
IntegrationHeartBeat.perform_heartbeat_check(heartbeat_id, integration_heartbeat_checkup.request.id)
@shared_dedicated_queue_retry_task()
def restore_heartbeat_tasks():
"""
Restore heartbeat tasks in case they got lost for some reason
"""
HeartBeat = apps.get_model("heartbeat", "HeartBeat")
for heartbeat in HeartBeat.objects.all():
if (
heartbeat.last_checkup_task_time
+ timezone.timedelta(minutes=5)
+ timezone.timedelta(seconds=heartbeat.timeout_seconds)
< timezone.now()
):
task = heartbeat_checkup.apply_async((heartbeat.pk,), countdown=5)
heartbeat.actual_check_up_task_id = task.id
heartbeat.save()
@shared_dedicated_queue_retry_task()
def process_heartbeat_task(alert_receive_channel_pk):
start = perf_counter()

View file

@ -34,10 +34,10 @@ class HeartBeatTextCreator:
def _get_heartbeat_expired_message(self):
heartbeat_docs_url = create_engine_url("/#/integrations/heartbeat", override_base=settings.DOCS_URL)
heartbeat_expired_message = (
f"Amixr was waiting for a heartbeat from {self.integration_verbal}. "
f"Heartbeat is missing. That could happen because {self.integration_verbal} stopped or"
f" there are connectivity issues between Amixr and {self.integration_verbal}. "
f"Read more in Amixr docs: {heartbeat_docs_url}"
f"Grafana OnCall was waiting for a heartbeat from {self.integration_verbal} "
f"and one was not received. This can happen when {self.integration_verbal} has stopped or "
f"there are connectivity issues between Grafana OnCall and {self.integration_verbal}. "
f"You can read more in the Grafana OnCall docs here: {heartbeat_docs_url}"
)
return heartbeat_expired_message
@ -46,7 +46,9 @@ class HeartBeatTextCreator:
return heartbeat_expired_title
def _get_heartbeat_restored_message(self):
heartbeat_expired_message = f"Amixr received a signal from {self.integration_verbal}. Heartbeat restored."
heartbeat_expired_message = (
f"Grafana OnCall received a signal from {self.integration_verbal}. Heartbeat has been restored."
)
return heartbeat_expired_message
def _get_heartbeat_instruction_template(self):
@ -59,9 +61,9 @@ class HeartBeatTextCreatorForTitleGrouping(HeartBeatTextCreator):
"""
def _get_heartbeat_expired_title(self):
heartbeat_expired_title = "Amixr heartbeat"
heartbeat_expired_title = "Grafana OnCall heartbeat"
return heartbeat_expired_title
def _get_heartbeat_restored_title(self):
heartbeat_expired_title = "Amixr heartbeat"
heartbeat_expired_title = "Grafana OnCall heartbeat"
return heartbeat_expired_title

View file

@ -12,7 +12,6 @@ from .views import (
AmazonSNS,
GrafanaAlertingAPIView,
GrafanaAPIView,
HeartBeatAPIView,
IntegrationHeartBeatAPIView,
UniversalAPIView,
)
@ -33,7 +32,6 @@ urlpatterns = [
path("grafana_alerting/<str:alert_channel_key>/", GrafanaAlertingAPIView.as_view(), name="grafana_alerting"),
path("alertmanager/<str:alert_channel_key>/", AlertManagerAPIView.as_view(), name="alertmanager"),
path("amazon_sns/<str:alert_channel_key>/", AmazonSNS.as_view(), name="amazon_sns"),
path("heartbeat/<str:alert_channel_key>/", HeartBeatAPIView.as_view(), name="heartbeat"),
path("alertmanager_v2/<str:alert_channel_key>/", AlertManagerV2View.as_view(), name="alertmanager_v2"),
path("<str:integration_type>/<str:alert_channel_key>/", UniversalAPIView.as_view(), name="universal"),
]

View file

@ -1,14 +1,9 @@
import json
import logging
from django.apps import apps
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.utils import IntegrityError
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.template import loader
from django.utils import timezone
from django.http import HttpResponseBadRequest, JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django_sns_view.views import SNSEndpoint
@ -16,7 +11,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from apps.alerts.models import AlertReceiveChannel
from apps.heartbeat.tasks import heartbeat_checkup, process_heartbeat_task
from apps.heartbeat.tasks import process_heartbeat_task
from apps.integrations.mixins import (
AlertChannelDefiningMixin,
BrowsableInstructionMixin,
@ -262,122 +257,6 @@ class UniversalAPIView(BrowsableInstructionMixin, AlertChannelDefiningMixin, Int
return Response("Ok.")
# TODO: restore HeartBeatAPIView integration or clean it up as it is not used now
class HeartBeatAPIView(AlertChannelDefiningMixin, APIView):
def get(self, request):
template = loader.get_template("heartbeat_link.html")
docs_url = create_engine_url("/#/integrations/heartbeat", override_base=settings.DOCS_URL)
return HttpResponse(
template.render(
{
"docs_url": docs_url,
}
)
)
def post(self, request):
alert_receive_channel = self.request.alert_receive_channel
HeartBeat = apps.get_model("heartbeat", "HeartBeat")
if request.data.get("action") == "activate":
# timeout_seconds
timeout_seconds = request.data.get("timeout_seconds")
try:
timeout_seconds = int(timeout_seconds)
except ValueError:
timeout_seconds = None
if timeout_seconds is None:
return Response(status=400, data="timeout_seconds int expected")
# id
_id = request.data.get("id", "default")
# title
title = request.data.get("title", "Title")
# title
link = request.data.get("link")
# message
message = request.data.get("message")
heartbeat = HeartBeat(
alert_receive_channel=alert_receive_channel,
timeout_seconds=timeout_seconds,
title=title,
message=message,
link=link,
user_defined_id=_id,
last_heartbeat_time=timezone.now(),
last_checkup_task_time=timezone.now(),
actual_check_up_task_id="none",
)
try:
heartbeat.save()
with transaction.atomic():
heartbeat = HeartBeat.objects.filter(pk=heartbeat.pk).select_for_update()[0]
task = heartbeat_checkup.apply_async(
(heartbeat.pk,),
countdown=heartbeat.timeout_seconds,
)
heartbeat.actual_check_up_task_id = task.id
heartbeat.save()
except IntegrityError:
return Response(status=400, data="id should be unique")
elif request.data.get("action") == "deactivate":
_id = request.data.get("id", "default")
try:
heartbeat = HeartBeat.objects.filter(
alert_receive_channel=alert_receive_channel,
user_defined_id=_id,
).get()
heartbeat.delete()
except HeartBeat.DoesNotExist:
return Response(status=400, data="heartbeat not found")
elif request.data.get("action") == "list":
result = []
heartbeats = HeartBeat.objects.filter(
alert_receive_channel=alert_receive_channel,
).all()
for heartbeat in heartbeats:
result.append(
{
"created_at": heartbeat.created_at,
"last_heartbeat": heartbeat.last_heartbeat_time,
"expiration_time": heartbeat.expiration_time,
"is_expired": heartbeat.is_expired,
"id": heartbeat.user_defined_id,
"title": heartbeat.title,
"timeout_seconds": heartbeat.timeout_seconds,
"link": heartbeat.link,
"message": heartbeat.message,
}
)
return Response(result)
elif request.data.get("action") == "heartbeat":
_id = request.data.get("id", "default")
with transaction.atomic():
try:
heartbeat = HeartBeat.objects.filter(
alert_receive_channel=alert_receive_channel,
user_defined_id=_id,
).select_for_update()[0]
task = heartbeat_checkup.apply_async(
(heartbeat.pk,),
countdown=heartbeat.timeout_seconds,
)
heartbeat.actual_check_up_task_id = task.id
heartbeat.last_heartbeat_time = timezone.now()
update_fields = ["actual_check_up_task_id", "last_heartbeat_time"]
state_changed = heartbeat.check_heartbeat_state()
if state_changed:
update_fields.append("previous_alerted_state_was_life")
heartbeat.save(update_fields=update_fields)
except IndexError:
return Response(status=400, data="heartbeat not found")
return Response("Ok.")
class IntegrationHeartBeatAPIView(AlertChannelDefiningMixin, IntegrationHeartBeatRateLimitMixin, APIView):
def get(self, request):
self._process_heartbeat_signal(request, request.alert_receive_channel)

View file

@ -415,11 +415,6 @@ ALERT_GROUP_ESCALATION_AUDITOR_CELERY_TASK_HEARTBEAT_URL = os.getenv(
)
CELERY_BEAT_SCHEDULE = {
"restore_heartbeat_tasks": {
"task": "apps.heartbeat.tasks.restore_heartbeat_tasks",
"schedule": 10 * 60,
"args": (),
},
"start_refresh_ical_final_schedules": {
"task": "apps.schedules.tasks.refresh_ical_files.start_refresh_ical_final_schedules",
"schedule": crontab(minute=15, hour=0),

View file

@ -51,10 +51,8 @@ CELERY_TASK_ROUTES = {
"apps.alerts.tasks.delete_alert_group.delete_alert_group": {"queue": "default"},
"apps.alerts.tasks.send_alert_group_signal.send_alert_group_signal": {"queue": "default"},
"apps.alerts.tasks.wipe.wipe": {"queue": "default"},
"apps.heartbeat.tasks.heartbeat_checkup": {"queue": "default"},
"apps.heartbeat.tasks.integration_heartbeat_checkup": {"queue": "default"},
"apps.heartbeat.tasks.process_heartbeat_task": {"queue": "default"},
"apps.heartbeat.tasks.restore_heartbeat_tasks": {"queue": "default"},
"apps.metrics_exporter.tasks.start_calculate_and_cache_metrics": {"queue": "default"},
"apps.metrics_exporter.tasks.start_recalculation_for_new_metric": {"queue": "default"},
"apps.metrics_exporter.tasks.save_organizations_ids_in_cache": {"queue": "default"},

View file

@ -16,12 +16,12 @@ import { UserActions } from 'utils/authorization';
const cx = cn.bind({});
interface IntegrationHearbeatFormProps {
interface IntegrationHeartbeatFormProps {
alertReceveChannelId: AlertReceiveChannel['id'];
onClose?: () => void;
}
const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: IntegrationHearbeatFormProps) => {
const IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: IntegrationHeartbeatFormProps) => {
const [interval, setInterval] = useState<number>(undefined);
const { heartbeatStore, alertReceiveChannelStore } = useStore();
@ -110,6 +110,6 @@ const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: Int
}
});
export default withMobXProviderContext(IntegrationHearbeatForm) as ({
export default withMobXProviderContext(IntegrationHeartbeatForm) as ({
alertReceveChannelId,
}: IntegrationHearbeatFormProps) => JSX.Element;
}: IntegrationHeartbeatFormProps) => JSX.Element;

View file

@ -43,7 +43,7 @@ import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu';
import EditRegexpRouteTemplateModal from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal';
import CollapsedIntegrationRouteDisplay from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay';
import ExpandedIntegrationRouteDisplay from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay';
import IntegrationHeartbeatForm from 'containers/IntegrationContainers/IntegrationHearbeatForm/IntegrationHeartbeatForm';
import IntegrationHeartbeatForm from 'containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm';
import IntegrationTemplateList from 'containers/IntegrationContainers/IntegrationTemplatesList';
import IntegrationForm from 'containers/IntegrationForm/IntegrationForm';
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
@ -742,7 +742,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
}>(undefined);
const [isIntegrationSettingsOpen, setIsIntegrationSettingsOpen] = useState(false);
const [isHearbeatFormOpen, setIsHearbeatFormOpen] = useState(false);
const [isHeartbeatFormOpen, setIsHeartbeatFormOpen] = useState(false);
const [isDemoModalOpen, setIsDemoModalOpen] = useState(false);
const [maintenanceData, setMaintenanceData] = useState<{
disabled: boolean;
@ -784,10 +784,10 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
/>
)}
{isHearbeatFormOpen && (
{isHeartbeatFormOpen && (
<IntegrationHeartbeatForm
alertReceveChannelId={alertReceiveChannel['id']}
onClose={() => setIsHearbeatFormOpen(false)}
onClose={() => setIsHeartbeatFormOpen(false)}
/>
)}
@ -823,7 +823,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
{showHeartbeatSettings() && (
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
<div className={cx('integration__actionItem')} onClick={() => setIsHearbeatFormOpen(true)}>
<div className={cx('integration__actionItem')} onClick={() => setIsHeartbeatFormOpen(true)}>
Heartbeat Settings
</div>
</WithPermissionControlTooltip>
@ -1117,7 +1117,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
/>
)}
{renderHearbeat(alertReceiveChannel)}
{renderHeartbeat(alertReceiveChannel)}
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px', marginLeft: '8px' }}>
<div className={cx('headerTop__item')}>
@ -1153,7 +1153,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
);
}
function renderHearbeat(alertReceiveChannel: AlertReceiveChannel) {
function renderHeartbeat(alertReceiveChannel: AlertReceiveChannel) {
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
const heartbeat = heartbeatStore.items[heartbeatId];

View file

@ -26,7 +26,9 @@ import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
import TeamName from 'containers/TeamName/TeamName';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { HeartIcon, HeartRedIcon } from 'icons';
import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel';
import { AlertReceiveChannel, MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
import { HeartbeatStore } from 'models/heartbeat/heartbeat';
import IntegrationHelper from 'pages/integration/Integration.helper';
import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
@ -343,7 +345,11 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
);
}
renderHeartbeat(item: AlertReceiveChannel, alertReceiveChannelStore, heartbeatStore) {
renderHeartbeat(
item: AlertReceiveChannel,
alertReceiveChannelStore: AlertReceiveChannelStore,
heartbeatStore: HeartbeatStore
) {
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];