Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maxim/bring heartbeats back to UI #2550

Merged
merged 10 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.instruction {
ol,
ul {
padding: 0;
margin: 0;
list-style: none;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SelectableValue } from '@grafana/data';
import { Button, Drawer, Field, HorizontalGroup, Select, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import Emoji from 'react-emoji-render';

import IntegrationInputField from 'components/IntegrationInputField/IntegrationInputField';
import Text from 'components/Text/Text';
Expand All @@ -14,7 +15,9 @@ import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore';
import { UserActions } from 'utils/authorization';

const cx = cn.bind({});
import styles from './IntegrationHeartbeatForm.module.scss';

const cx = cn.bind(styles);

interface IntegrationHeartbeatFormProps {
alertReceveChannelId: AlertReceiveChannel['id'];
Expand All @@ -27,16 +30,16 @@ const IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: In
const { heartbeatStore, alertReceiveChannelStore } = useStore();

const alertReceiveChannel = alertReceiveChannelStore.items[alertReceveChannelId];
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
const heartbeat = heartbeatStore.items[heartbeatId];

useEffect(() => {
heartbeatStore.updateTimeoutOptions();
}, [heartbeatStore]);
}, []);

useEffect(() => {
if (alertReceiveChannel.heartbeat) {
setInterval(alertReceiveChannel.heartbeat.timeout_seconds);
}
}, [alertReceiveChannel]);
setInterval(heartbeat.timeout_seconds);
}, [heartbeat]);

const timeoutOptions = heartbeatStore.timeoutOptions;

Expand Down Expand Up @@ -66,22 +69,30 @@ const IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: In
</WithPermissionControlTooltip>
</Field>
</div>

<div className={cx('u-width-100')}>
<Field label="Endpoint" description="Use the following unique Grafana link to send GET and POST requests">
<IntegrationInputField value={alertReceiveChannel?.integration_url} showEye={false} isMasked={false} />
<IntegrationInputField value={heartbeat?.link} showEye={false} isMasked={false} />
</Field>
</div>
<p className={cx('instruction')}>
To send periodic heartbeat alerts from <Emoji text={alertReceiveChannel?.verbal_name || ''} /> to OnCall, do
the following:
<span
dangerouslySetInnerHTML={{
__html: heartbeat.instruction,
}}
/>
</p>
</VerticalGroup>

<VerticalGroup style={{ marginTop: 'auto' }}>
<HorizontalGroup className={cx('buttons')} justify="flex-end">
<Button variant={'secondary'} onClick={onClose}>
Cancel
Close
</Button>
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
<Button variant="primary" onClick={onSave}>
{alertReceiveChannel.heartbeat ? 'Save' : 'Create'}
Update
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
Expand All @@ -91,24 +102,14 @@ const IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: In
);

async function onSave() {
const heartbeat = alertReceiveChannel.heartbeat;

if (heartbeat) {
await heartbeatStore.saveHeartbeat(heartbeat.id, {
alert_receive_channel: heartbeat.alert_receive_channel,
timeout_seconds: interval,
});

onClose();
} else {
await heartbeatStore.createHeartbeat(alertReceveChannelId, {
timeout_seconds: interval,
});
await heartbeatStore.saveHeartbeat(heartbeat.id, {
alert_receive_channel: heartbeat.alert_receive_channel,
timeout_seconds: interval,
});

onClose();
}
onClose();

await alertReceiveChannelStore.updateItem(alertReceveChannelId);
await alertReceiveChannelStore.loadItem(alertReceveChannelId);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,14 @@ export class AlertReceiveChannelStore extends BaseStore {
async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling = false): Promise<AlertReceiveChannel> {
const alertReceiveChannel = await this.getById(id, skipErrorHandling);

// @ts-ignore
this.items = {
...this.items,
[id]: alertReceiveChannel,
[id]: omit(alertReceiveChannel, 'heartbeat'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we store heartbeat on the objects within items? why do we need alertReceiveChannelToHeartbeat`? I believe a heartbeat always belongs to a single alert receive channel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in my mind heartbeat is a separate entity (it even has it's own id and ideally it should have separate endpoint), that's why we have 'models/heartbeat', we need alertReceiveChannelToHeartbeat to save appropriate heartbeat Id for each alert receive channel, it's one to one mapping. Yes we can store heartbeat id just in alert receive channel object, but I did not want to pollute alert receive channel object

};

this.populateHearbeats([alertReceiveChannel]);

return alertReceiveChannel;
}

Expand All @@ -116,33 +119,9 @@ export class AlertReceiveChannelStore extends BaseStore {
),
};

this.searchResult = results.map((item: AlertReceiveChannel) => item.id);

const heartbeats = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat;
}
this.populateHearbeats(results);

return acc;
}, {});

this.rootStore.heartbeatStore.items = {
...this.rootStore.heartbeatStore.items,
...heartbeats,
};

const alertReceiveChannelToHeartbeat = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
}

return acc;
}, {});

this.alertReceiveChannelToHeartbeat = {
...this.alertReceiveChannelToHeartbeat,
...alertReceiveChannelToHeartbeat,
};
this.searchResult = results.map((item: AlertReceiveChannel) => item.id);

this.updateCounters();

Expand All @@ -164,13 +143,20 @@ export class AlertReceiveChannelStore extends BaseStore {
),
};

this.paginatedSearchResult = results.map((item: AlertReceiveChannel) => item.id);
this.populateHearbeats(results);

this.paginatedSearchResult = {
count,
results: results.map((item: AlertReceiveChannel) => item.id),
};

const heartbeats = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
this.updateCounters();

return results;
}

populateHearbeats(alertReceiveChannels: AlertReceiveChannel[]) {
const heartbeats = alertReceiveChannels.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat;
}
Expand All @@ -183,22 +169,21 @@ export class AlertReceiveChannelStore extends BaseStore {
...heartbeats,
};

const alertReceiveChannelToHeartbeat = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
}
const alertReceiveChannelToHeartbeat = alertReceiveChannels.reduce(
(acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
}

return acc;
}, {});
return acc;
},
{}
);

this.alertReceiveChannelToHeartbeat = {
...this.alertReceiveChannelToHeartbeat,
...alertReceiveChannelToHeartbeat,
};

this.updateCounters();

return results;
}

@action
Expand Down
17 changes: 6 additions & 11 deletions grafana-plugin/src/pages/integration/Integration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
alertReceiveChannel,
changeIsTemplateSettingsOpen,
}) => {
const { alertReceiveChannelStore, heartbeatStore } = useStore();
const { alertReceiveChannelStore } = useStore();

const history = useHistory();

Expand Down Expand Up @@ -927,9 +927,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
);

function showHeartbeatSettings() {
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
const heartbeat = heartbeatStore.items[heartbeatId];
return !!heartbeat?.last_heartbeat_time_verbal;
return alertReceiveChannel.is_available_for_integration_heartbeat;
}

function deleteIntegration() {
Expand Down Expand Up @@ -1159,22 +1157,19 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
const heartbeat = heartbeatStore.items[heartbeatId];

const heartbeatStatus = Boolean(heartbeat?.status);

if (
!alertReceiveChannel.is_available_for_integration_heartbeat ||
!alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal
) {
if (!alertReceiveChannel.is_available_for_integration_heartbeat || !heartbeat?.last_heartbeat_time_verbal) {
return null;
}

const heartbeatStatus = Boolean(heartbeat?.status);

return (
<TooltipBadge
text={undefined}
className={cx('heartbeat-badge')}
borderType={heartbeatStatus ? 'success' : 'danger'}
customIcon={heartbeatStatus ? <HeartIcon /> : <HeartRedIcon />}
tooltipTitle={`Last heartbeat: ${alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal}`}
tooltipTitle={`Last heartbeat: ${heartbeat?.last_heartbeat_time_verbal}`}
tooltipContent={undefined}
/>
);
Expand Down
14 changes: 5 additions & 9 deletions grafana-plugin/src/pages/integrations/Integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ 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';
Expand Down Expand Up @@ -128,7 +126,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
render() {
const { store, query } = this.props;
const { alertReceiveChannelId, page, confirmationModal } = this.state;
const { grafanaTeamStore, alertReceiveChannelStore, heartbeatStore } = store;
const { grafanaTeamStore, alertReceiveChannelStore } = store;

const { count, results } = alertReceiveChannelStore.getPaginatedSearchResult();

Expand Down Expand Up @@ -162,7 +160,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
width: '5%',
title: 'Heartbeat',
key: 'heartbeat',
render: (item: AlertReceiveChannel) => this.renderHeartbeat(item, alertReceiveChannelStore, heartbeatStore),
render: (item: AlertReceiveChannel) => this.renderHeartbeat(item),
},
{
width: '15%',
Expand Down Expand Up @@ -345,11 +343,9 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
);
}

renderHeartbeat(
item: AlertReceiveChannel,
alertReceiveChannelStore: AlertReceiveChannelStore,
heartbeatStore: HeartbeatStore
) {
renderHeartbeat(item: AlertReceiveChannel) {
const { store } = this.props;
const { alertReceiveChannelStore, heartbeatStore } = store;
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];

const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
Expand Down