From 6da36b3c0bf0beed92fd242ecd10d00d7532a3ab Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Tue, 20 Feb 2024 13:09:22 +0100 Subject: [PATCH] Use autogenerated types for alert_receive_channels (#3851) # What this PR does - autogenerate new types exposed by backend, remove custom types that duplicate autogenerated ones - use autogenerated types for alert receive channels - in alert_receive_channel model: - use autogenerate http client (`onCallApi`) for http requests - extract methods that don't update state into alert_receive_channel.helpers.ts and make them pure (they accept AlertReceiveChannelStore as param) to avoid inconsistency and issues with `this` binding - use `makeAutoObservable` - remove unneeded decorators - rename update* methods to fetch* whenever such methods retrieve data from backend with GET requests - in other models use `@action.bound` for actions and arrow functions for store methods that are not actions (in subsequent PRs we will apply the same changes as in alert_receive_channel, this is just for now until we do it) - refactor http-client so that it shows global notification on http errors automatically and provide the possibility to opt-out from it when making a call - improve type-safety of `GSelect` - fix bug related to attaching alert group (https://raintank-corp.slack.com/archives/C04JCU51NF8/p1707476487580579) ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/3331 ## 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: Vadim Stepanov --- CHANGELOG.md | 1 + dev/README.md | 4 +- .../api/serializers/alert_receive_channel.py | 2 +- .../apps/api/views/alert_receive_channel.py | 6 +- grafana-plugin/.gitignore | 2 +- grafana-plugin/package.json | 3 +- .../IntegrationContactPoint.tsx | 19 +- .../IntegrationHowToConnect.tsx | 4 +- .../IntegrationSendDemoAlertModal.tsx | 13 +- .../components/Policy/EscalationPolicy.tsx | 135 +- .../AlertReceiveChannelCard.tsx | 7 +- .../parts/connectors/MSTeamsConnector.tsx | 8 +- .../parts/connectors/SlackConnector.tsx | 8 +- .../parts/connectors/TelegramConnector.tsx | 8 +- .../AttachIncidentForm/AttachIncidentForm.tsx | 31 +- .../ChannelFilterForm/ChannelFilterForm.tsx | 7 +- .../EditRegexpRouteTemplateModal.tsx | 9 +- .../EscalationChainForm.tsx | 8 +- .../src/containers/GSelect/GSelect.tsx | 53 +- .../GrafanaTeamSelect/GrafanaTeamSelect.tsx | 6 +- .../HeartbeatModal/HeartbeatForm.tsx | 4 +- .../CollapsedIntegrationRouteDisplay.tsx | 4 +- .../ExpandedIntegrationRouteDisplay.tsx | 6 +- .../IntegrationHeartbeatForm.tsx | 6 +- .../IntegrationTemplatesList.tsx | 8 +- .../IntegrationForm.config.tsx | 11 +- .../IntegrationForm.helpers.ts | 4 +- .../IntegrationForm/IntegrationForm.tsx | 91 +- .../IntegrationLabelsForm.helpers.test.ts | 5 +- .../IntegrationLabelsForm.helpers.ts | 10 +- .../IntegrationLabelsForm.tsx | 21 +- .../IntegrationTemplate.tsx | 6 +- .../MaintenanceForm.config.tsx | 14 +- .../MaintenanceForm/MaintenanceForm.tsx | 16 +- .../OutgoingWebhookForm.config.tsx | 27 +- .../OutgoingWebhookForm.tsx | 142 +- .../ScheduleForm/ScheduleForm.config.ts | 179 +- .../containers/ScheduleForm/ScheduleForm.tsx | 17 +- .../TemplatePreview/TemplatePreview.tsx | 11 +- .../TemplateResult/TemplateResult.tsx | 6 +- .../TemplatesAlertGroupsList.tsx | 4 +- .../alert_receive_channel.helpers.ts | 189 +- .../alert_receive_channel.ts | 427 +-- .../alert_receive_channel.types.ts | 68 +- .../alert_receive_channel_filters.ts | 6 +- .../src/models/alertgroup/alertgroup.ts | 72 +- .../src/models/alertgroup/alertgroup.types.ts | 4 +- .../src/models/api_token/api_token.ts | 6 +- grafana-plugin/src/models/base_store.ts | 10 +- .../channel_filter/channel_filter.types.ts | 4 +- grafana-plugin/src/models/cloud/cloud.ts | 6 +- .../src/models/direct_paging/direct_paging.ts | 12 +- .../escalation_chain/escalation_chain.ts | 16 +- .../escalation_policy/escalation_policy.ts | 10 +- grafana-plugin/src/models/filters/filters.ts | 10 +- .../models/global_setting/global_setting.ts | 8 +- .../src/models/grafana_team/grafana_team.ts | 6 +- .../src/models/heartbeat/heartbeat.ts | 10 +- .../src/models/heartbeat/heartbeat.types.ts | 4 +- grafana-plugin/src/models/label/label.ts | 4 +- grafana-plugin/src/models/loader/loader.ts | 4 + .../models/msteams_channel/msteams_channel.ts | 10 +- .../outgoing_webhook/outgoing_webhook.ts | 12 +- .../src/models/schedule/schedule.ts | 40 +- grafana-plugin/src/models/slack/slack.ts | 8 +- .../src/models/slack_channel/slack_channel.ts | 10 +- .../telegram_channel/telegram_channel.ts | 14 +- .../src/models/timezone/timezone.helpers.ts | 2 - grafana-plugin/src/models/user/user.test.ts | 2 +- grafana-plugin/src/models/user/user.ts | 48 +- .../src/models/user_group/user_group.ts | 6 +- .../oncall-api/autogenerated-api.types.d.ts | 2785 ++++++++++++++++- .../network/oncall-api/http-client.test.ts | 9 +- .../src/network/oncall-api/http-client.ts | 149 +- .../src/pages/incident/Incident.tsx | 12 +- .../src/pages/incidents/Incidents.tsx | 6 +- .../pages/integration/Integration.helper.ts | 10 +- .../src/pages/integration/Integration.tsx | 85 +- .../src/pages/integrations/Integrations.tsx | 85 +- .../tabs/SlackSettings/SlackSettings.tsx | 21 +- .../state/rootBaseStore/RootBaseStore.test.ts | 8 +- .../src/state/rootBaseStore/RootBaseStore.ts | 6 +- grafana-plugin/src/utils/types.ts | 15 + grafana-plugin/src/utils/utils.ts | 16 +- 84 files changed, 3914 insertions(+), 1247 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c19181c..8ae7c232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Check for permissions on Slack escalate command ([#3891](https://github.com/grafana/oncall/pull/3891)) +- Use autogenerated types on the frontend for alert receive channels ([#3331](https://github.com/grafana/oncall/issues/3331)) - Update OnCall Insights dashboard @Ferril ([#3875](https://github.com/grafana/oncall/pull/3875)) - Do not delete webhook if its team is deleted @mderynck ([#3873](https://github.com/grafana/oncall/pull/3873)) - Update user details internal API perms ([#3900](https://github.com/grafana/oncall/pull/3900)) diff --git a/dev/README.md b/dev/README.md index bf2effb0..62c868ed 100644 --- a/dev/README.md +++ b/dev/README.md @@ -518,11 +518,11 @@ In order to automate types creation and prevent API usage pitfalls, OnCall proje ```ts import { ApiSchemas } from "network/oncall-api/api.types"; - import onCallApi from "network/oncall-api/http-client"; + import { onCallApi } from "network/oncall-api/http-client"; const { data: { results }, - } = await onCallApi.GET("/alertgroups/"); + } = await onCallApi().GET("/alertgroups/"); const alertGroups: Array = results; ``` diff --git a/engine/apps/api/serializers/alert_receive_channel.py b/engine/apps/api/serializers/alert_receive_channel.py index 5f1e0f82..d7d01b45 100644 --- a/engine/apps/api/serializers/alert_receive_channel.py +++ b/engine/apps/api/serializers/alert_receive_channel.py @@ -241,7 +241,7 @@ class AlertReceiveChannelSerializer( demo_alert_payload = serializers.JSONField(source="config.example_payload", read_only=True) routes_count = serializers.SerializerMethodField() connected_escalations_chains_count = serializers.SerializerMethodField() - inbound_email = serializers.CharField(required=False) + inbound_email = serializers.CharField(required=False, read_only=True) is_legacy = serializers.SerializerMethodField() alert_group_labels = IntegrationAlertGroupLabelsSerializer(source="*", required=False) diff --git a/engine/apps/api/views/alert_receive_channel.py b/engine/apps/api/views/alert_receive_channel.py index 8fda334f..b118b532 100644 --- a/engine/apps/api/views/alert_receive_channel.py +++ b/engine/apps/api/views/alert_receive_channel.py @@ -511,7 +511,11 @@ class AlertReceiveChannelView( fields={ "uid": serializers.CharField(), "name": serializers.CharField(), - "contact_points": serializers.ListField(child=serializers.CharField()), + "contact_points": inline_serializer( + "AlertReceiveChannelConnectedContactPointsInner", + fields={"name": serializers.CharField(), "notification_connected": serializers.BooleanField()}, + many=True, + ), }, many=True, ) diff --git a/grafana-plugin/.gitignore b/grafana-plugin/.gitignore index 9d0e1126..c10b9a5f 100644 --- a/grafana-plugin/.gitignore +++ b/grafana-plugin/.gitignore @@ -15,6 +15,6 @@ yarn-error.log* grafana-plugin.yml # playwright -/playwright-report/ +/playwright-report* /playwright/.cache/ /e2e-tests/storageState.json diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index dfdc90c8..e8994569 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -27,7 +27,8 @@ "ci-report": "grafana-toolkit plugin:ci-report", "start": "yarn watch", "plop": "plop", - "setversion": "setversion" + "setversion": "setversion", + "typecheck": "tsc --noEmit" }, "repository": { "type": "git", diff --git a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx index dedf6727..7a127489 100644 --- a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx +++ b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx @@ -21,7 +21,9 @@ import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; import { Tag } from 'components/Tag/Tag'; import { Text } from 'components/Text/Text'; import { WithConfirm } from 'components/WithConfirm/WithConfirm'; -import { AlertReceiveChannel, ContactPoint } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; +import { ContactPoint } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import styles from 'pages/integration/Integration.module.scss'; import { useStore } from 'state/useStore'; import { getVar } from 'utils/DOM'; @@ -46,7 +48,7 @@ interface IntegrationContactPointState { } export const IntegrationContactPoint: React.FC<{ - id: AlertReceiveChannel['id']; + id: ApiSchemas['AlertReceiveChannel']['id']; }> = observer(({ id }) => { const { alertReceiveChannelStore } = useStore(); const contactPoints = alertReceiveChannelStore.connectedContactPoints[id]; @@ -84,7 +86,7 @@ export const IntegrationContactPoint: React.FC<{ useEffect(() => { (async function () { - const response = await alertReceiveChannelStore.getGrafanaAlertingContactPoints(); + const response = await AlertReceiveChannelHelper.getGrafanaAlertingContactPoints(); setState({ allContactPoints: response, dataSourceOptions: response.map((res) => ({ label: res.name, value: res.uid })), @@ -281,12 +283,11 @@ export const IntegrationContactPoint: React.FC<{ aria-label="Disconnect Contact Point" name="trash-alt" onClick={() => { - alertReceiveChannelStore - .disconnectContactPoint(id, item.dataSourceId, item.contactPoint) + AlertReceiveChannelHelper.disconnectContactPoint(id, item.dataSourceId, item.contactPoint) .then(() => { closeDrawer(); openNotification('Contact point has been removed'); - alertReceiveChannelStore.updateConnectedContactPoints(id); + alertReceiveChannelStore.fetchConnectedContactPoints(id); }) .catch(() => openErrorNotification('An error has occurred. Please try again.')); }} @@ -338,13 +339,13 @@ export const IntegrationContactPoint: React.FC<{ setState({ isLoading: true }); (isExistingContactPoint - ? alertReceiveChannelStore.connectContactPoint(id, selectedAlertManager, selectedContactPoint) - : alertReceiveChannelStore.createContactPoint(id, selectedAlertManager, selectedContactPoint) + ? AlertReceiveChannelHelper.connectContactPoint(id, selectedAlertManager, selectedContactPoint) + : AlertReceiveChannelHelper.createContactPoint(id, selectedAlertManager, selectedContactPoint) ) .then(() => { closeDrawer(); openNotification('A new contact point has been connected to your integration'); - alertReceiveChannelStore.updateConnectedContactPoints(id); + alertReceiveChannelStore.fetchConnectedContactPoints(id); }) .catch((ex) => { const error = ex.response?.data?.detail ?? 'An error has occurred. Please try again.'; diff --git a/grafana-plugin/src/components/IntegrationHowToConnect/IntegrationHowToConnect.tsx b/grafana-plugin/src/components/IntegrationHowToConnect/IntegrationHowToConnect.tsx index 9827f66f..e4db7b57 100644 --- a/grafana-plugin/src/components/IntegrationHowToConnect/IntegrationHowToConnect.tsx +++ b/grafana-plugin/src/components/IntegrationHowToConnect/IntegrationHowToConnect.tsx @@ -8,14 +8,14 @@ import { IntegrationInputField } from 'components/IntegrationInputField/Integrat import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; import { Tag } from 'components/Tag/Tag'; import { Text } from 'components/Text/Text'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import styles from 'pages/integration/Integration.module.scss'; import { useStore } from 'state/useStore'; import { getVar } from 'utils/DOM'; const cx = cn.bind(styles); -export const IntegrationHowToConnect: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id }) => { +export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveChannel']['id'] }> = ({ id }) => { const { alertReceiveChannelStore } = useStore(); const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id]; const hasAlerts = !!alertReceiveChannelCounter?.alerts_count; diff --git a/grafana-plugin/src/components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal.tsx b/grafana-plugin/src/components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal.tsx index d0f43ace..fd44fcd0 100644 --- a/grafana-plugin/src/components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal.tsx +++ b/grafana-plugin/src/components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal.tsx @@ -10,7 +10,8 @@ import { MonacoEditor, MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEdi import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.config'; import { PluginLink } from 'components/PluginLink/PluginLink'; import { Text } from 'components/Text/Text'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import styles from 'pages/integration/Integration.module.scss'; import { useStore } from 'state/useStore'; import { openNotification } from 'utils/utils'; @@ -19,7 +20,7 @@ const cx = cn.bind(styles); interface IntegrationSendDemoPayloadModalProps { isOpen: boolean; - alertReceiveChannel: AlertReceiveChannel; + alertReceiveChannel: ApiSchemas['AlertReceiveChannel']; onHideOrCancel: () => void; } @@ -88,7 +89,7 @@ export const IntegrationSendDemoAlertModal: React.FC openNotification('CURL has been copied')}> - @@ -100,14 +101,14 @@ export const IntegrationSendDemoAlertModal: React.FC { - alertReceiveChannelStore.updateCounters(); + AlertReceiveChannelHelper.sendDemoAlert(alertReceiveChannel.id, parsedPayload).then(() => { + alertReceiveChannelStore.fetchCounters(); openNotification(); onHideOrCancel(); }); diff --git a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx index 1b681011..7b6e3481 100644 --- a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx @@ -20,10 +20,12 @@ import { EscalationPolicy as EscalationPolicyType, EscalationPolicyOption, } from 'models/escalation_policy/escalation_policy.types'; -import { GrafanaTeamStore } from 'models/grafana_team/grafana_team'; -import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook'; -import { ScheduleStore } from 'models/schedule/schedule'; -import { SelectOption } from 'state/types'; +import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; +import { Schedule } from 'models/schedule/schedule.types'; +import { User } from 'models/user/user.types'; +import { UserGroup } from 'models/user_group/user_group.types'; +import { SelectOption, WithStoreProps } from 'state/types'; +import { withMobXProviderContext } from 'state/withStore'; import { getVar } from 'utils/DOM'; import { UserActions } from 'utils/authorization/authorization'; @@ -34,7 +36,7 @@ import styles from './EscalationPolicy.module.css'; const cx = cn.bind(styles); -interface ElementSortableProps { +interface ElementSortableProps extends WithStoreProps { index: number; } @@ -51,9 +53,6 @@ export interface EscalationPolicyProps extends ElementSortableProps { backgroundClassName?: string; backgroundHexNumber?: string; isSlackInstalled: boolean; - teamStore: GrafanaTeamStore; - outgoingWebhookStore: OutgoingWebhookStore; - scheduleStore: ScheduleStore; } class _EscalationPolicy extends React.Component { @@ -81,13 +80,13 @@ class _EscalationPolicy extends React.Component { )} {escalationOption && reactStringReplace(escalationOption.display_name, /\{\{([^}]+)\}\}/g, this.replacePlaceholder)} - {this._renderNote()} + {this.renderNote()} {is_final || isDisabled ? null : ( { case 'timerange': return this.renderTimeRange(); case 'users': - return this._renderNotifyToUsersQueue(); + return this.renderNotifyToUsersQueue(); case 'wait_delay': - return this._renderWaitDelays(); + return this.renderWaitDelays(); case 'slack_user_group': - return this._renderNotifyUserGroup(); + return this.renderNotifyUserGroup(); case 'schedule': - return this._renderNotifySchedule(); + return this.renderNotifySchedule(); case 'custom_webhook': - return this._renderTriggerCustomWebhook(); + return this.renderTriggerCustomWebhook(); case 'num_alerts_in_window': return this.renderNumAlertsInWindow(); case 'num_minutes_in_window': @@ -124,7 +123,7 @@ class _EscalationPolicy extends React.Component { } }; - _renderNote() { + renderNote() { const { data, isSlackInstalled, escalationChoices } = this.props; const { step } = data; @@ -152,32 +151,39 @@ class _EscalationPolicy extends React.Component { } } - private _renderNotifyToUsersQueue() { - const { data, isDisabled } = this.props; + renderNotifyToUsersQueue() { + const { + data, + isDisabled, + store: { userStore }, + } = this.props; const { notify_to_users_queue } = data; return ( - isMulti showSearch allowClear disabled={isDisabled} - modelName="userStore" displayField="username" valueField="pk" placeholder="Select Users" className={cx('select', 'control', 'multiSelect')} value={notify_to_users_queue} - onChange={this._getOnChangeHandler('notify_to_users_queue')} + onChange={this.getOnChangeHandler('notify_to_users_queue')} getOptionLabel={({ value }: SelectableValue) => } width={'auto'} + items={userStore.items} + fetchItemsFn={userStore.updateItems} + fetchItemFn={userStore.updateItem} + getSearchResult={userStore.getSearchResult} /> ); } - private renderImportance() { + renderImportance() { const { data, isDisabled } = this.props; const { important } = data; @@ -189,7 +195,7 @@ class _EscalationPolicy extends React.Component { disabled={isDisabled} value={Number(important)} // @ts-ignore - onChange={this._getOnSelectChangeHandler('important')} + onChange={this.getOnSelectChangeHandler('important')} options={[ { value: 0, @@ -222,7 +228,7 @@ class _EscalationPolicy extends React.Component { ); } - private renderTimeRange() { + renderTimeRange() { const { data, isDisabled } = this.props; return ( @@ -231,14 +237,14 @@ class _EscalationPolicy extends React.Component { from={data.from_time} to={data.to_time} disabled={isDisabled} - onChange={this._getOnTimeRangeChangeHandler()} + onChange={this.getOnTimeRangeChangeHandler()} className={cx('select', 'control')} /> ); } - private _renderWaitDelays() { + renderWaitDelays() { const { data, isDisabled, waitDelays = [] } = this.props; const { wait_delay } = data; @@ -251,7 +257,7 @@ class _EscalationPolicy extends React.Component { className={cx('select', 'control')} // @ts-ignore value={wait_delay} - onChange={this._getOnSelectChangeHandler('wait_delay')} + onChange={this.getOnSelectChangeHandler('wait_delay')} options={waitDelays.map((waitDelay: SelectOption) => ({ value: waitDelay.value, label: waitDelay.display_name, @@ -262,7 +268,7 @@ class _EscalationPolicy extends React.Component { ); } - private renderNumAlertsInWindow() { + renderNumAlertsInWindow() { const { data, isDisabled } = this.props; const { num_alerts_in_window } = data; @@ -273,7 +279,7 @@ class _EscalationPolicy extends React.Component { disabled={isDisabled} className={cx('control')} value={num_alerts_in_window} - onChange={this._getOnInputChangeHandler('num_alerts_in_window')} + onChange={this.getOnInputChangeHandler('num_alerts_in_window')} ref={(node) => { if (node) { node.setAttribute('type', 'number'); @@ -285,7 +291,7 @@ class _EscalationPolicy extends React.Component { ); } - private renderNumMinutesInWindowOptions() { + renderNumMinutesInWindowOptions() { const { data, isDisabled, numMinutesInWindowOptions = [] } = this.props; const { num_minutes_in_window } = data; @@ -298,7 +304,7 @@ class _EscalationPolicy extends React.Component { className={cx('select', 'control')} // @ts-ignore value={num_minutes_in_window} - onChange={this._getOnSelectChangeHandler('num_minutes_in_window')} + onChange={this.getOnSelectChangeHandler('num_minutes_in_window')} options={numMinutesInWindowOptions.map((waitDelay: SelectOption) => ({ value: waitDelay.value, label: waitDelay.display_name, @@ -308,25 +314,31 @@ class _EscalationPolicy extends React.Component { ); } - private _renderNotifySchedule() { - const { data, isDisabled, teamStore, scheduleStore } = this.props; + renderNotifySchedule() { + const { + data, + isDisabled, + store: { grafanaTeamStore, scheduleStore }, + } = this.props; const { notify_schedule } = data; return ( - showSearch allowClear disabled={isDisabled} - modelName="scheduleStore" + items={scheduleStore.items} + fetchItemsFn={scheduleStore.updateItems} + getSearchResult={scheduleStore.getSearchResult} displayField="name" valueField="id" placeholder="Select Schedule" className={cx('select', 'control')} value={notify_schedule} - onChange={this._getOnChangeHandler('notify_schedule')} + onChange={this.getOnChangeHandler('notify_schedule')} getOptionLabel={(item: SelectableValue) => { - const team = teamStore.items[scheduleStore.items[item.value].team]; + const team = grafanaTeamStore.items[scheduleStore.items[item.value].team]; return ( <> {item.label} @@ -339,45 +351,58 @@ class _EscalationPolicy extends React.Component { ); } - private _renderNotifyUserGroup() { - const { data, isDisabled } = this.props; + renderNotifyUserGroup() { + const { + data, + isDisabled, + store: { userGroupStore }, + } = this.props; const { notify_to_group } = data; return ( - disabled={isDisabled} - modelName="userGroupStore" + items={userGroupStore.items} + fetchItemsFn={userGroupStore.updateItems} + getSearchResult={userGroupStore.getSearchResult} displayField="name" valueField="id" placeholder="Select User Group" className={cx('select', 'control')} value={notify_to_group} - onChange={this._getOnChangeHandler('notify_to_group')} + onChange={this.getOnChangeHandler('notify_to_group')} width={'auto'} /> ); } - private _renderTriggerCustomWebhook() { - const { data, isDisabled, teamStore, outgoingWebhookStore } = this.props; + renderTriggerCustomWebhook() { + const { + data, + isDisabled, + store: { grafanaTeamStore, outgoingWebhookStore }, + } = this.props; const { custom_webhook } = data; return ( - showSearch disabled={isDisabled} - modelName="outgoingWebhookStore" + items={outgoingWebhookStore.items} + fetchItemsFn={outgoingWebhookStore.updateItems} + fetchItemFn={outgoingWebhookStore.updateItem} + getSearchResult={outgoingWebhookStore.getSearchResult} displayField="name" valueField="id" placeholder="Select Webhook" className={cx('select', 'control')} value={custom_webhook} - onChange={this._getOnChangeHandler('custom_webhook')} + onChange={this.getOnChangeHandler('custom_webhook')} getOptionLabel={(item: SelectableValue) => { - const team = teamStore.items[outgoingWebhookStore.items[item.value].team]; + const team = grafanaTeamStore.items[outgoingWebhookStore.items[item.value].team]; return ( <> {item.label} @@ -395,7 +420,7 @@ class _EscalationPolicy extends React.Component { ); } - _getOnSelectChangeHandler = (field: string) => { + getOnSelectChangeHandler = (field: string) => { return (option: SelectableValue) => { const { data, onChange = () => {} } = this.props; const { id } = data; @@ -409,7 +434,7 @@ class _EscalationPolicy extends React.Component { }; }; - _getOnInputChangeHandler = (field: string) => { + getOnInputChangeHandler = (field: string) => { const { data, onChange = () => {} } = this.props; const { id } = data; @@ -423,7 +448,7 @@ class _EscalationPolicy extends React.Component { }; }; - _getOnChangeHandler = (field: string) => { + getOnChangeHandler = (field: string) => { return (value: any) => { const { data, onChange = () => {} } = this.props; const { id } = data; @@ -437,7 +462,7 @@ class _EscalationPolicy extends React.Component { }; }; - _getOnTimeRangeChangeHandler() { + getOnTimeRangeChangeHandler() { return (value: string[]) => { const { data, onChange = () => {} } = this.props; const { id } = data; @@ -452,11 +477,13 @@ class _EscalationPolicy extends React.Component { }; } - _handleDelete = () => { + handleDelete = () => { const { onDelete, data } = this.props; onDelete(data); }; } -export const EscalationPolicy = SortableElement(_EscalationPolicy) as React.ComponentClass; +export const EscalationPolicy = withMobXProviderContext( + SortableElement(_EscalationPolicy) as React.ComponentClass +); diff --git a/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx b/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx index 61664de4..23f27641 100644 --- a/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx +++ b/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx @@ -11,7 +11,8 @@ import { PluginLink } from 'components/PluginLink/PluginLink'; import { Text } from 'components/Text/Text'; import { TeamName } from 'containers/TeamName/TeamName'; import { HeartGreenIcon, HeartRedIcon } from 'icons/Icons'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { useStore } from 'state/useStore'; import styles from './AlertReceiveChannelCard.module.scss'; @@ -19,7 +20,7 @@ import styles from './AlertReceiveChannelCard.module.scss'; const cx = cn.bind(styles); interface AlertReceiveChannelCardProps { - id: AlertReceiveChannel['id']; + id: ApiSchemas['AlertReceiveChannel']['id']; onShowHeartbeatModal: () => void; } @@ -38,7 +39,7 @@ export const AlertReceiveChannelCard = observer((props: AlertReceiveChannelCardP const heartbeatStatus = Boolean(heartbeat?.status); - const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel); + const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel); return (
diff --git a/grafana-plugin/src/containers/AlertRules/parts/connectors/MSTeamsConnector.tsx b/grafana-plugin/src/containers/AlertRules/parts/connectors/MSTeamsConnector.tsx index 6ee3c534..92e062c1 100644 --- a/grafana-plugin/src/containers/AlertRules/parts/connectors/MSTeamsConnector.tsx +++ b/grafana-plugin/src/containers/AlertRules/parts/connectors/MSTeamsConnector.tsx @@ -22,7 +22,7 @@ export const MSTeamsConnector = (props: MSTeamsConnectorProps) => { const { channelFilterId } = props; const store = useStore(); - const { alertReceiveChannelStore } = store; + const { alertReceiveChannelStore, msteamsChannelStore } = store; const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId]; @@ -54,11 +54,13 @@ export const MSTeamsConnector = (props: MSTeamsConnectorProps) => {
Post to Microsoft Teams channel - showSearch allowClear className={cx('select', 'control')} - modelName="msteamsChannelStore" + items={msteamsChannelStore.items} + fetchItemsFn={msteamsChannelStore.updateItems} + getSearchResult={msteamsChannelStore.getSearchResult} displayField="display_name" valueField="id" placeholder="Select Microsoft Teams Channel" diff --git a/grafana-plugin/src/containers/AlertRules/parts/connectors/SlackConnector.tsx b/grafana-plugin/src/containers/AlertRules/parts/connectors/SlackConnector.tsx index a7ae9aa1..745e9702 100644 --- a/grafana-plugin/src/containers/AlertRules/parts/connectors/SlackConnector.tsx +++ b/grafana-plugin/src/containers/AlertRules/parts/connectors/SlackConnector.tsx @@ -29,6 +29,7 @@ export const SlackConnector = (props: SlackConnectorProps) => { const { organizationStore: { currentOrganization }, alertReceiveChannelStore, + slackChannelStore, } = store; const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId]; @@ -56,11 +57,14 @@ export const SlackConnector = (props: SlackConnectorProps) => { Slack Channel - showSearch allowClear className={cx('select', 'control')} - modelName="slackChannelStore" + items={slackChannelStore.items} + fetchItemsFn={slackChannelStore.updateItems} + fetchItemFn={slackChannelStore.updateItem} + getSearchResult={slackChannelStore.getSearchResult} displayField="display_name" valueField="id" placeholder="Select Slack Channel" diff --git a/grafana-plugin/src/containers/AlertRules/parts/connectors/TelegramConnector.tsx b/grafana-plugin/src/containers/AlertRules/parts/connectors/TelegramConnector.tsx index f4edb70a..b02a6e5f 100644 --- a/grafana-plugin/src/containers/AlertRules/parts/connectors/TelegramConnector.tsx +++ b/grafana-plugin/src/containers/AlertRules/parts/connectors/TelegramConnector.tsx @@ -20,7 +20,7 @@ interface TelegramConnectorProps { export const TelegramConnector = ({ channelFilterId }: TelegramConnectorProps) => { const store = useStore(); - const { alertReceiveChannelStore } = store; + const { alertReceiveChannelStore, telegramChannelStore } = store; const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId]; @@ -46,11 +46,13 @@ export const TelegramConnector = ({ channelFilterId }: TelegramConnectorProps) = Post to telegram channel - showSearch allowClear className={cx('select', 'control')} - modelName="telegramChannelStore" + items={telegramChannelStore.items} + fetchItemsFn={telegramChannelStore.updateItems} + getSearchResult={telegramChannelStore.getSearchResult} displayField="channel_name" valueField="id" placeholder="Select Telegram Channel" diff --git a/grafana-plugin/src/containers/AttachIncidentForm/AttachIncidentForm.tsx b/grafana-plugin/src/containers/AttachIncidentForm/AttachIncidentForm.tsx index eccaab0a..6d07b39f 100644 --- a/grafana-plugin/src/containers/AttachIncidentForm/AttachIncidentForm.tsx +++ b/grafana-plugin/src/containers/AttachIncidentForm/AttachIncidentForm.tsx @@ -27,6 +27,17 @@ interface GroupedAlertNumberProps { value: Alert['pk']; } +const GroupedAlertNumber = observer(({ value }: GroupedAlertNumberProps) => { + const { alertGroupStore } = useStore(); + const alert = alertGroupStore.items[value]; + + return ( +
+ #{alert?.inside_organization_number} {alert?.render_for_web?.title} +
+ ); +}); + export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachIncidentFormProps) => { const store = useStore(); @@ -45,17 +56,6 @@ export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachInci }); }, [selected, alertGroupStore, id, onHide, onUpdate]); - const GroupedAlertNumber = observer(({ value }: GroupedAlertNumberProps) => { - const { alertGroupStore } = useStore(); - const alert = alertGroupStore.items[value]; - - return ( -
- #{alert?.inside_organization_number} {alert?.render_for_web?.title} -
- ); - }); - return ( - showSearch - modelName="alertGroupStore" + items={alertGroupStore.items} + fetchItemsFn={alertGroupStore.fetchItemsAvailableForAttachment} + fetchItemFn={alertGroupStore.updateItem} + getSearchResult={alertGroupStore.getSearchResult} valueField="pk" displayField="render_for_web.title" - placeholder="Select Incident" + placeholder="Select Alert Group" className={cx('select', 'control')} filterOptions={(optionId) => optionId !== id} value={selected} diff --git a/grafana-plugin/src/containers/ChannelFilterForm/ChannelFilterForm.tsx b/grafana-plugin/src/containers/ChannelFilterForm/ChannelFilterForm.tsx index ad769474..ab55a146 100644 --- a/grafana-plugin/src/containers/ChannelFilterForm/ChannelFilterForm.tsx +++ b/grafana-plugin/src/containers/ChannelFilterForm/ChannelFilterForm.tsx @@ -9,8 +9,9 @@ import { Block } from 'components/GBlock/Block'; import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor'; import { Text } from 'components/Text/Text'; import { IncidentMatcher } from 'containers/IncidentMatcher/IncidentMatcher'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; import { ChannelFilter, FilteringTermType } from 'models/channel_filter/channel_filter.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils/utils'; @@ -20,7 +21,7 @@ const cx = cn.bind(styles); interface ChannelFilterFormProps { id: ChannelFilter['id'] | 'new'; - alertReceiveChannelId: AlertReceiveChannel['id']; + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']; onHide: () => void; onUpdate: (channelFilterId: ChannelFilter['id']) => void; data?: ChannelFilter; @@ -63,7 +64,7 @@ export const ChannelFilterForm = observer((props: ChannelFilterFormProps) => { const onUpdateClickCallback = useCallback(() => { (id === 'new' - ? alertReceiveChannelStore.createChannelFilter({ + ? AlertReceiveChannelHelper.createChannelFilter({ alert_receive_channel: alertReceiveChannelId, filtering_term: filteringTerm, filtering_term_type: filteringTermType, diff --git a/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx index 8ef223d9..f60c7625 100644 --- a/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx +++ b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx @@ -9,8 +9,9 @@ import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesF import { Block } from 'components/GBlock/Block'; import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor'; import { Text } from 'components/Text/Text'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils/utils'; @@ -21,7 +22,7 @@ const cx = cn.bind(styles); interface EditRegexpRouteTemplateModalProps { channelFilterId: ChannelFilter['id']; template?: TemplateForEdit; - alertReceiveChannelId?: AlertReceiveChannel['id']; + alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id']; onHide: () => void; onUpdateRoute: (values: any, channelFilterId: ChannelFilter['id'], type: number) => void; onOpenEditIntegrationTemplate?: (templateName: string, channelFilterId: ChannelFilter['id']) => void; @@ -59,14 +60,14 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp }, [regexpTemplateBody]); const handleConvertToJinja2 = useCallback(() => { - alertReceiveChannelStore.convertRegexpTemplateToJinja2Template(channelFilterId).then((response) => { + AlertReceiveChannelHelper.convertRegexpTemplateToJinja2Template(channelFilterId).then((response) => { alertReceiveChannelStore .saveChannelFilter(channelFilterId, { filtering_term: response?.filtering_term_as_jinja2, filtering_term_type: 1, }) .then(() => { - alertReceiveChannelStore.updateChannelFilters(alertReceiveChannelId, true).then(() => { + alertReceiveChannelStore.fetchChannelFilters(alertReceiveChannelId, true).then(() => { onOpenEditIntegrationTemplate('route_template', channelFilterId); }); }); diff --git a/grafana-plugin/src/containers/EscalationChainForm/EscalationChainForm.tsx b/grafana-plugin/src/containers/EscalationChainForm/EscalationChainForm.tsx index fc81bf23..0894b6c8 100644 --- a/grafana-plugin/src/containers/EscalationChainForm/EscalationChainForm.tsx +++ b/grafana-plugin/src/containers/EscalationChainForm/EscalationChainForm.tsx @@ -30,7 +30,7 @@ export const EscalationChainForm: FC = (props) => { const { escalationChainId, onHide, onSubmit: onSubmitProp, mode } = props; const store = useStore(); - const { escalationChainStore, userStore } = store; + const { escalationChainStore, userStore, grafanaTeamStore } = store; const user = userStore.currentUser; @@ -92,8 +92,10 @@ export const EscalationChainForm: FC = (props) => {
- + items={grafanaTeamStore.items} + fetchItemsFn={grafanaTeamStore.updateItems} + getSearchResult={grafanaTeamStore.getSearchResult} displayField="name" valueField="id" showSearch diff --git a/grafana-plugin/src/containers/GSelect/GSelect.tsx b/grafana-plugin/src/containers/GSelect/GSelect.tsx index eda69d91..d6585dc4 100644 --- a/grafana-plugin/src/containers/GSelect/GSelect.tsx +++ b/grafana-plugin/src/containers/GSelect/GSelect.tsx @@ -6,23 +6,24 @@ import cn from 'classnames/bind'; import { get, isNil } from 'lodash-es'; import { observer } from 'mobx-react'; -import { BaseStore } from 'models/base_store'; -import { RootBaseStore } from 'state/rootBaseStore/RootBaseStore'; -import { useStore } from 'state/useStore'; import { useDebouncedCallback } from 'utils/hooks'; -import { PropertiesThatExtendsAnotherClass } from 'utils/types'; import styles from './GSelect.module.scss'; const cx = cn.bind(styles); -interface GSelectProps { +interface GSelectProps { + items: { + [key: string]: Item; + }; + fetchItemsFn: (query?: string) => Promise; + fetchItemFn?: (id: string) => Promise; + getSearchResult: (query?: string) => Item[] | { page_size: number; count: number; results: Item[] }; placeholder: string; isLoading?: boolean; value?: string | string[] | null; defaultValue?: string | string[] | null; onChange: (value: string, item: any) => void; - modelName: PropertiesThatExtendsAnotherClass; autoFocus?: boolean; defaultOpen?: boolean; disabled?: boolean; @@ -44,7 +45,7 @@ interface GSelectProps { icon?: string; } -export const GSelect = observer((props: GSelectProps) => { +export const GSelect = observer((props: GSelectProps) => { const { autoFocus, showSearch = false, @@ -58,7 +59,6 @@ export const GSelect = observer((props: GSelectProps) => { onChange, disabled, showError, - modelName, displayField = 'display_name', valueField = 'id', isMulti = false, @@ -68,34 +68,37 @@ export const GSelect = observer((props: GSelectProps) => { filterOptions, width = null, icon = null, + items: propItems, + fetchItemsFn, + fetchItemFn, + getSearchResult, } = props; - const store = useStore(); - const model = (store as any)[modelName]; - const onChangeCallback = useCallback( (option) => { if (isMulti) { const values = option.map((option: SelectableValue) => option.value); - const items = option.map((option: SelectableValue) => model.items[option.value]); + const items = option.map((option: SelectableValue) => propItems[option.value]); onChange(values, items); } else { if (option) { const id = option.value; - const item = model.items[id]; + const item = propItems[id]; onChange(id, item); } else { onChange(null, null); } } }, - [model, onChange] + [propItems, onChange] ); const loadOptions = useDebouncedCallback((query: string, cb) => { - model.updateItems(query).then(() => { - const searchResult = model.getSearchResult(query); + fetchItemsFn(query).then(() => { + const searchResult = getSearchResult(query); + // TODO: we need to unify interface of search results to get rid of ts-ignore + // @ts-ignore let items = Array.isArray(searchResult.results) ? searchResult.results : searchResult; if (filterOptions) { items = items.filter((opt: any) => filterOptions(opt[valueField])); @@ -112,19 +115,17 @@ export const GSelect = observer((props: GSelectProps) => { const values = isMulti ? (value ? (value as string[]) : []) - .filter((id) => id in model.items) + .filter((id) => id in propItems) .map((id: string) => ({ value: id, - label: get(model.items[id], displayField), - description: getDescription && getDescription(model.items[id]), + label: get(propItems[id], displayField), + description: getDescription && getDescription(propItems[id]), })) - : model.items[value as string] + : propItems[value as string] ? { value, - label: get(model.items[value as string], displayField) - ? get(model.items[value as string], displayField) - : 'hidden', - description: getDescription && getDescription(model.items[value as string]), + label: get(propItems[value as string], displayField) ? get(propItems[value as string], displayField) : 'hidden', + description: getDescription && getDescription(propItems[value as string]), } : value; @@ -132,8 +133,8 @@ export const GSelect = observer((props: GSelectProps) => { const values = isMulti ? value : [value]; (values ? (values as string[]) : []).forEach((value: string) => { - if (!isNil(value) && !model.items[value] && model.updateItem) { - model.updateItem(value, true); + if (!isNil(value) && !propItems[value] && fetchItemFn) { + fetchItemFn(value); } }); }, [value]); diff --git a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx index d4fee804..c40c09b6 100644 --- a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx +++ b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx @@ -52,9 +52,11 @@ export const GrafanaTeamSelect = observer( } const select = ( - showSearch - modelName="grafanaTeamStore" + items={grafanaTeamStore.items} + fetchItemsFn={grafanaTeamStore.updateItems} + getSearchResult={grafanaTeamStore.getSearchResult} displayField="name" valueField="id" placeholder="Select team" diff --git a/grafana-plugin/src/containers/HeartbeatModal/HeartbeatForm.tsx b/grafana-plugin/src/containers/HeartbeatModal/HeartbeatForm.tsx index 1b37d115..d121ba3b 100644 --- a/grafana-plugin/src/containers/HeartbeatModal/HeartbeatForm.tsx +++ b/grafana-plugin/src/containers/HeartbeatModal/HeartbeatForm.tsx @@ -9,7 +9,7 @@ import Emoji from 'react-emoji-render'; import { Text } from 'components/Text/Text'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { HeartGreenIcon, HeartRedIcon } from 'icons/Icons'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { SelectOption } from 'state/types'; import { useStore } from 'state/useStore'; import { withMobXProviderContext } from 'state/withStore'; @@ -20,7 +20,7 @@ import styles from './HeartbeatForm.module.css'; const cx = cn.bind(styles); interface HeartBeatModalProps { - alertReceveChannelId: AlertReceiveChannel['id']; + alertReceveChannelId: ApiSchemas['AlertReceiveChannel']['id']; onUpdate: () => void; } diff --git a/grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx b/grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx index 5531ff19..6642c88f 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx @@ -10,8 +10,8 @@ import { Text } from 'components/Text/Text'; import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge'; import styles from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss'; import { RouteButtonsDisplay } from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper'; import { IntegrationHelper } from 'pages/integration/Integration.helper'; import { useStore } from 'state/useStore'; @@ -19,7 +19,7 @@ import { useStore } from 'state/useStore'; const cx = cn.bind(styles); interface CollapsedIntegrationRouteDisplayProps { - alertReceiveChannelId: AlertReceiveChannel['id']; + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']; channelFilterId: ChannelFilter['id']; routeIndex: number; toggle: () => void; diff --git a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx index 392198dd..2239c3b3 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx @@ -33,10 +33,10 @@ import { EscalationChainSteps } from 'containers/EscalationChainSteps/Escalation import styles from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss'; import { TeamName } from 'containers/TeamName/TeamName'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper'; import { IntegrationHelper } from 'pages/integration/Integration.helper'; import { MONACO_INPUT_HEIGHT_SMALL } from 'pages/integration/IntegrationCommon.config'; @@ -47,7 +47,7 @@ import { openNotification } from 'utils/utils'; const cx = cn.bind(styles); interface ExpandedIntegrationRouteDisplayProps { - alertReceiveChannelId: AlertReceiveChannel['id']; + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']; channelFilterId: ChannelFilter['id']; routeIndex: number; templates: AlertTemplatesDTO[]; @@ -372,7 +372,7 @@ const ReadOnlyEscalationChain: React.FC<{ escalationChainId: string }> = ({ esca }; interface RouteButtonsDisplayProps { - alertReceiveChannelId: AlertReceiveChannel['id']; + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']; channelFilterId: ChannelFilter['id']; routeIndex: number; setRouteIdForDeletion(): void; diff --git a/grafana-plugin/src/containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm.tsx b/grafana-plugin/src/containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm.tsx index f337be90..bc83a419 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react'; import { IntegrationInputField } from 'components/IntegrationInputField/IntegrationInputField'; import { Text } from 'components/Text/Text'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { SelectOption } from 'state/types'; import { useStore } from 'state/useStore'; import { withMobXProviderContext } from 'state/withStore'; @@ -20,7 +20,7 @@ import styles from './IntegrationHeartbeatForm.module.scss'; const cx = cn.bind(styles); interface IntegrationHeartbeatFormProps { - alertReceveChannelId: AlertReceiveChannel['id']; + alertReceveChannelId: ApiSchemas['AlertReceiveChannel']['id']; onClose?: () => void; } @@ -117,7 +117,7 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I openNotification('Heartbeat settings have been updated'); - await alertReceiveChannelStore.loadItem(alertReceveChannelId); + await alertReceiveChannelStore.fetchItemById(alertReceveChannelId); } }); diff --git a/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.tsx b/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.tsx index d0bf5541..cd48112a 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.tsx @@ -10,8 +10,8 @@ import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor'; import { MONACO_READONLY_CONFIG } from 'components/MonacoEditor/MonacoEditor.config'; import { Text } from 'components/Text/Text'; import { getTemplatesToRender } from 'containers/IntegrationContainers/IntegrationTemplatesList.config'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { IntegrationHelper } from 'pages/integration/Integration.helper'; import styles from 'pages/integration/Integration.module.scss'; import { MONACO_INPUT_HEIGHT_TALL } from 'pages/integration/IntegrationCommon.config'; @@ -22,7 +22,7 @@ const cx = cn.bind(styles); interface IntegrationTemplateListProps { templates: AlertTemplatesDTO[]; - alertReceiveChannelId: AlertReceiveChannel['id']; + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']; openEditTemplateModal: (templateName: string | string[]) => void; alertReceiveChannelIsBasedOnAlertManager: boolean; alertReceiveChannelAllowSourceBasedResolving: boolean; @@ -37,9 +37,9 @@ export const IntegrationTemplateList: React.FC = o alertReceiveChannelAllowSourceBasedResolving, }) => { const { alertReceiveChannelStore, features } = useStore(); - const [isRestoringTemplate, setIsRestoringTemplate] = useState(false); + const [isRestoringTemplate, setIsRestoringTemplate] = useState(false); const [templateRestoreName, setTemplateRestoreName] = useState(undefined); - const [autoresolveValue, setAutoresolveValue] = useState(alertReceiveChannelAllowSourceBasedResolving); + const [autoresolveValue, setAutoresolveValue] = useState(alertReceiveChannelAllowSourceBasedResolving); const handleSaveClick = useCallback((event: React.ChangeEvent) => { setAutoresolveValue(event.target.checked); diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.tsx b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.tsx index 5edbb7f2..bdf161af 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.tsx +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.tsx @@ -2,10 +2,11 @@ import React from 'react'; import { Icon, Label, Tooltip } from '@grafana/ui'; -import { FormItem, FormItemType } from 'components/GForm/GForm.types'; +import { FormItemType } from 'components/GForm/GForm.types'; +import { GrafanaTeamStore } from 'models/grafana_team/grafana_team'; import { generateAssignToTeamInputDescription } from 'utils/consts'; -export const form: { name: string; fields: FormItem[] } = { +export const getForm = (grafanaTeamStore: GrafanaTeamStore) => ({ name: 'Integration', fields: [ { @@ -33,7 +34,9 @@ export const form: { name: string; fields: FormItem[] } = { ), type: FormItemType.GSelect, extra: { - modelName: 'grafanaTeamStore', + items: grafanaTeamStore.items, + fetchItemsFn: grafanaTeamStore.updateItems, + getSearchResult: grafanaTeamStore.getSearchResult, displayField: 'name', valueField: 'id', showSearch: true, @@ -77,4 +80,4 @@ export const form: { name: string; fields: FormItem[] } = { render: true, }, ], -}; +}); diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.helpers.ts b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.helpers.ts index 78193ec9..5db6b598 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.helpers.ts +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.helpers.ts @@ -1,6 +1,6 @@ -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; -export function prepareForEdit(item: AlertReceiveChannel) { +export function prepareForEdit(item: ApiSchemas['AlertReceiveChannel']) { return { verbal_name: item.verbal_name, description_short: item.description_short, diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx index 562378fe..252263fe 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx @@ -1,4 +1,4 @@ -import React, { useState, ChangeEvent, useEffect, useReducer, useRef } from 'react'; +import React, { useState, ChangeEvent, useEffect, useReducer, useRef, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; import { @@ -26,10 +26,8 @@ import { PluginLink } from 'components/PluginLink/PluginLink'; import { Text } from 'components/Text/Text'; import { Labels } from 'containers/Labels/Labels'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { - AlertReceiveChannel, - AlertReceiveChannelOption, -} from 'models/alert_receive_channel/alert_receive_channel.types'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { IntegrationHelper } from 'pages/integration/Integration.helper'; import { AppFeature } from 'state/features'; import { useStore } from 'state/useStore'; @@ -37,18 +35,18 @@ import { UserActions } from 'utils/authorization/authorization'; import { PLUGIN_ROOT } from 'utils/consts'; import { openErrorNotification } from 'utils/utils'; -import { form } from './IntegrationForm.config'; +import { getForm } from './IntegrationForm.config'; import { prepareForEdit } from './IntegrationForm.helpers'; import styles from './IntegrationForm.module.scss'; const cx = cn.bind(styles); interface IntegrationFormProps { - id: AlertReceiveChannel['id'] | 'new'; + id: ApiSchemas['AlertReceiveChannel']['id'] | 'new'; isTableView?: boolean; onHide: () => void; onSubmit: () => Promise; - navigateToAlertGroupLabels: (id: AlertReceiveChannel['id']) => void; + navigateToAlertGroupLabels: (id: ApiSchemas['AlertReceiveChannel']['id']) => void; } export const IntegrationForm = observer((props: IntegrationFormProps) => { @@ -61,18 +59,21 @@ export const IntegrationForm = observer((props: IntegrationFormProps) => { const { alertReceiveChannelStore, userStore: { currentUser: user }, + grafanaTeamStore, } = store; const [filterValue, setFilterValue] = useState(''); const [showNewIntegrationForm, setShowNewIntegrationForm] = useState(false); - const [selectedOption, setSelectedOption] = useState(undefined); + const [selectedOption, setSelectedOption] = useState(undefined); const [showIntegrarionsListDrawer, setShowIntegrarionsListDrawer] = useState(id === 'new'); const [allContactPoints, setAllContactPoints] = useState([]); const [errors, setErrors] = useState>(); + const form = useMemo(() => getForm(grafanaTeamStore), [grafanaTeamStore]); + useEffect(() => { (async function () { - setAllContactPoints(await alertReceiveChannelStore.getGrafanaAlertingContactPoints()); + setAllContactPoints(await AlertReceiveChannelHelper.getGrafanaAlertingContactPoints()); })(); }, []); @@ -84,7 +85,7 @@ export const IntegrationForm = observer((props: IntegrationFormProps) => { const { alertReceiveChannelOptions } = alertReceiveChannelStore; const options = alertReceiveChannelOptions - ? alertReceiveChannelOptions.filter((option: AlertReceiveChannelOption) => { + ? alertReceiveChannelOptions.filter((option: ApiSchemas['AlertReceiveChannelIntegrationOptions']) => { if (option.value === 'grafana_alerting' && !window.grafanaBootData.settings.unifiedAlertingEnabled) { return false; } @@ -219,58 +220,36 @@ export const IntegrationForm = observer((props: IntegrationFormProps) => { if (isCreate) { await createNewIntegration(); } else { - await alertReceiveChannelStore.update(id, data, undefined, true); + await alertReceiveChannelStore.update({ id, data, skipErrorHandling: true }); } } catch (error) { - setErrors(error.response.data); - - openErrorNotification( - `There was an issue ${isCreate ? 'creating' : 'updating'} the integration. Please try again.` - ); + setErrors(error); return; } await onSubmit(); onHide(); - function createNewIntegration(): Promise { - let promise = alertReceiveChannelStore.create(data, true); - + async function createNewIntegration(): Promise { + const response = await alertReceiveChannelStore.create({ data, skipErrorHandling: true }); const pushHistory = (id) => history.push(`${PLUGIN_ROOT}/integrations/${id}`); - - promise - .then((response) => { - if (!response) { - return; - } - - if (!IntegrationHelper.isSpecificIntegration(selectedOption.value, 'grafana_alerting')) { - return pushHistory(response.id); - } - - return ( - data.is_existing - ? alertReceiveChannelStore.connectContactPoint(response.id, data.alert_manager, data.contact_point) - : alertReceiveChannelStore.createContactPoint(response.id, data.alert_manager, data.contact_point) - ) - .catch(onCatch) - .finally(() => pushHistory(response.id)); - }) - .catch(onCatch); - - return promise; - } - - function onCatch(err: any) { - if (err.response?.data?.length > 0) { - openErrorNotification(err.response.data); - } else { - openErrorNotification('Something went wrong, please try again later.'); + if (!response) { + return; } + + if (!IntegrationHelper.isSpecificIntegration(selectedOption.value, 'grafana_alerting')) { + pushHistory(response.id); + } + + await (data.is_existing + ? AlertReceiveChannelHelper.connectContactPoint + : AlertReceiveChannelHelper.createContactPoint)(response.id, data.alert_manager, data.contact_point); + + pushHistory(response.id); } } - function onBlockClick(option: AlertReceiveChannelOption) { + function onBlockClick(option: ApiSchemas['AlertReceiveChannelIntegrationOptions']) { setSelectedOption(option); setShowNewIntegrationForm(true); setShowIntegrarionsListDrawer(false); @@ -338,11 +317,9 @@ const CustomFieldSectionRenderer: React.FC = ({ } ); - const { alertReceiveChannelStore } = useStore(); - useEffect(() => { (async function () { - const response = await alertReceiveChannelStore.getGrafanaAlertingContactPoints(); + const response = await AlertReceiveChannelHelper.getGrafanaAlertingContactPoints(); setState({ allContactPoints: response, dataSources: response.map((res) => ({ label: res.name, value: res.uid })), @@ -445,7 +422,9 @@ const CustomFieldSectionRenderer: React.FC = ({ } }; -const HowTheIntegrationWorks: React.FC<{ selectedOption: AlertReceiveChannelOption }> = ({ selectedOption }) => { +const HowTheIntegrationWorks: React.FC<{ selectedOption: ApiSchemas['AlertReceiveChannelIntegrationOptions'] }> = ({ + selectedOption, +}) => { if (!selectedOption) { return null; } @@ -487,8 +466,8 @@ const HowTheIntegrationWorks: React.FC<{ selectedOption: AlertReceiveChannelOpti }; const IntegrationBlocks: React.FC<{ - options: AlertReceiveChannelOption[]; - onBlockClick: (option: AlertReceiveChannelOption) => void; + options: Array; + onBlockClick: (option: ApiSchemas['AlertReceiveChannelIntegrationOptions']) => void; }> = ({ options, onBlockClick }) => { return (
diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.test.ts b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.test.ts index 73c69188..8ec071f0 100644 --- a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.test.ts +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.test.ts @@ -1,7 +1,10 @@ import { getIsTooManyLabelsWarningVisible } from './IntegrationLabelsForm.helpers'; describe('getIsTooManyLabelsWarningVisible()', () => { - const CUSTOM_LABEL = { key: { id: 'c', name: 'c' }, value: { id: 'c', name: 'c' } }; + const CUSTOM_LABEL = { + key: { id: 'c', name: 'c', prescribed: false }, + value: { id: 'c', name: 'c', prescribed: false }, + }; it('should return false if limit is not exceeded', () => { expect( diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts index f1d9d8fd..debd9ea1 100644 --- a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts @@ -1,6 +1,8 @@ -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; -const countNumberOfInheritedAndCustomLabels = (alert_group_labels: AlertReceiveChannel['alert_group_labels']) => { +const countNumberOfInheritedAndCustomLabels = ( + alert_group_labels: ApiSchemas['AlertReceiveChannel']['alert_group_labels'] +) => { const inheritedCount = alert_group_labels.inheritable ? Object.keys(alert_group_labels.inheritable).filter((labelKey) => alert_group_labels.inheritable?.[labelKey]) .length @@ -10,11 +12,11 @@ const countNumberOfInheritedAndCustomLabels = (alert_group_labels: AlertReceiveC }; export const getIsTooManyLabelsWarningVisible = ( - alert_group_labels: AlertReceiveChannel['alert_group_labels'], + alert_group_labels: ApiSchemas['AlertReceiveChannel']['alert_group_labels'], limit = 15 ) => countNumberOfInheritedAndCustomLabels(alert_group_labels) > limit; -export const getIsAddBtnDisabled = ({ custom }: AlertReceiveChannel['alert_group_labels']) => { +export const getIsAddBtnDisabled = ({ custom }: ApiSchemas['AlertReceiveChannel']['alert_group_labels']) => { const lastItem = custom.at(-1); return lastItem && (lastItem?.key.id === undefined || lastItem?.value.id === undefined); }; diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx index 73d5f979..561971c3 100644 --- a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx @@ -21,7 +21,6 @@ import { PluginLink } from 'components/PluginLink/PluginLink'; import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; import { Text } from 'components/Text/Text'; import { IntegrationTemplate } from 'containers/IntegrationTemplate/IntegrationTemplate'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { splitToGroups } from 'models/label/label.helpers'; import { LabelsErrors } from 'models/label/label.types'; import { ApiSchemas } from 'network/oncall-api/api.types'; @@ -39,10 +38,10 @@ const cx = cn.bind(styles); const INPUT_WIDTH = 280; interface IntegrationLabelsFormProps { - id: AlertReceiveChannel['id']; + id: ApiSchemas['AlertReceiveChannel']['id']; onSubmit: () => void; onHide: () => void; - onOpenIntegrationSettings: (id: AlertReceiveChannel['id']) => void; + onOpenIntegrationSettings: (id: ApiSchemas['AlertReceiveChannel']['id']) => void; } export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => { @@ -63,7 +62,9 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps const handleSave = async () => { try { - await alertReceiveChannelStore.saveAlertReceiveChannel(id, { alert_group_labels: alertGroupLabels }); + await alertReceiveChannelStore.saveAlertReceiveChannel(id, { + alert_group_labels: alertGroupLabels, + }); onSubmit(); onHide(); } catch (err) { @@ -246,9 +247,9 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps }); interface CustomLabelsProps { - alertGroupLabels: AlertReceiveChannel['alert_group_labels']; + alertGroupLabels: ApiSchemas['AlertReceiveChannel']['alert_group_labels']; customLabelsErrors: LabelsErrors; - onChange: (value: AlertReceiveChannel['alert_group_labels']) => void; + onChange: (value: ApiSchemas['AlertReceiveChannel']['alert_group_labels']) => void; onShowTemplateEditor: (index: number) => void; } @@ -263,8 +264,8 @@ const CustomLabels = (props: CustomLabelsProps) => { custom: [ ...alertGroupLabels.custom, { - key: { id: undefined, name: undefined }, - value: { id: undefined, name: undefined }, + key: { id: undefined, name: undefined, prescribed: false }, + value: { id: undefined, name: undefined, prescribed: false }, }, ], }); @@ -275,8 +276,8 @@ const CustomLabels = (props: CustomLabelsProps) => { custom: [ ...alertGroupLabels.custom, { - key: { id: undefined, name: undefined }, - value: { id: null, name: undefined }, // id = null means it's a templated value + key: { id: undefined, name: undefined, prescribed: false }, + value: { id: null, name: undefined, prescribed: false }, // id = null means it's a templated value }, ], }); diff --git a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx index 525acc9a..4bbb804e 100644 --- a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx +++ b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx @@ -20,10 +20,10 @@ import { Text } from 'components/Text/Text'; import { TemplateResult } from 'containers/TemplateResult/TemplateResult'; import { TemplatesAlertGroupsList, TEMPLATE_PAGE } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates'; import { Alert } from 'models/alertgroup/alertgroup.types'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { IntegrationTemplateOptions, LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config'; import { useStore } from 'state/useStore'; import { LocationHelper } from 'utils/LocationHelper'; @@ -34,7 +34,7 @@ import styles from './IntegrationTemplate.module.scss'; const cx = cn.bind(styles); interface IntegrationTemplateProps { - id: AlertReceiveChannel['id']; + id: ApiSchemas['AlertReceiveChannel']['id']; channelFilterId?: ChannelFilter['id']; template: TemplateForEdit; templateBody: string; @@ -49,7 +49,7 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) => const [isCheatSheetVisible, setIsCheatSheetVisible] = useState(false); const [chatOpsPermalink, setChatOpsPermalink] = useState(undefined); - const [alertGroupPayload, setAlertGroupPayload] = useState(undefined); + const [alertGroupPayload, setAlertGroupPayload] = useState<{ [key: string]: unknown }>(undefined); const [changedTemplateBody, setChangedTemplateBody] = useState(templateBody); const [resultError, setResultError] = useState(undefined); const [isRecentAlertGroupExisting, setIsRecentAlertGroupExisting] = useState(false); diff --git a/grafana-plugin/src/containers/MaintenanceForm/MaintenanceForm.config.tsx b/grafana-plugin/src/containers/MaintenanceForm/MaintenanceForm.config.tsx index 60a1729f..fcd02b44 100644 --- a/grafana-plugin/src/containers/MaintenanceForm/MaintenanceForm.config.tsx +++ b/grafana-plugin/src/containers/MaintenanceForm/MaintenanceForm.config.tsx @@ -3,10 +3,12 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; import Emoji from 'react-emoji-render'; -import { FormItem, FormItemType } from 'components/GForm/GForm.types'; +import { FormItemType } from 'components/GForm/GForm.types'; +import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; import { MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types'; -export const form: { name: string; fields: FormItem[] } = { +export const getForm = (alertReceiveChannelStore: AlertReceiveChannelStore) => ({ name: 'Maintenance', fields: [ { @@ -15,11 +17,15 @@ export const form: { name: string; fields: FormItem[] } = { type: FormItemType.GSelect, validation: { required: true }, extra: { - modelName: 'alertReceiveChannelStore', + items: alertReceiveChannelStore.items, + fetchItemsFn: alertReceiveChannelStore.fetchItems, + fetchItemFn: alertReceiveChannelStore.fetchItemById, + getSearchResult: () => AlertReceiveChannelHelper.getSearchResult(alertReceiveChannelStore), displayField: 'verbal_name', valueField: 'id', showSearch: true, getOptionLabel: (item: SelectableValue) => , + disabled: undefined, }, }, { @@ -77,4 +83,4 @@ export const form: { name: string; fields: FormItem[] } = { }, }, ], -}; +}); diff --git a/grafana-plugin/src/containers/MaintenanceForm/MaintenanceForm.tsx b/grafana-plugin/src/containers/MaintenanceForm/MaintenanceForm.tsx index fd6f2e8f..f4c73c26 100644 --- a/grafana-plugin/src/containers/MaintenanceForm/MaintenanceForm.tsx +++ b/grafana-plugin/src/containers/MaintenanceForm/MaintenanceForm.tsx @@ -7,12 +7,13 @@ import { observer } from 'mobx-react'; import { GForm } from 'components/GForm/GForm'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { useStore } from 'state/useStore'; import { UserActions } from 'utils/authorization/authorization'; import { openNotification, showApiError } from 'utils/utils'; -import { form } from './MaintenanceForm.config'; +import { getForm } from './MaintenanceForm.config'; import styles from './MaintenanceForm.module.css'; @@ -20,7 +21,7 @@ const cx = cn.bind(styles); interface MaintenanceFormProps { initialData: { - alert_receive_channel_id?: AlertReceiveChannel['id']; + alert_receive_channel_id?: ApiSchemas['AlertReceiveChannel']['id']; disabled?: boolean; }; onHide: () => void; @@ -29,20 +30,17 @@ interface MaintenanceFormProps { export const MaintenanceForm = observer((props: MaintenanceFormProps) => { const { onUpdate, onHide, initialData = {} } = props; + const { alertReceiveChannelStore } = useStore(); + const form = useMemo(() => getForm(alertReceiveChannelStore), [alertReceiveChannelStore]); const maintenanceForm = useMemo(() => (initialData.disabled ? cloneDeep(form) : form), [initialData]); - const store = useStore(); - - const { alertReceiveChannelStore } = store; - const handleSubmit = useCallback(async (data) => { try { - await alertReceiveChannelStore.startMaintenanceMode( + await AlertReceiveChannelHelper.startMaintenanceMode( initialData.alert_receive_channel_id, data.mode, data.duration ); - onHide(); onUpdate(); openNotification('Maintenance has been started'); diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.tsx b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.tsx index d752c481..ae046b2e 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.tsx @@ -4,6 +4,9 @@ import { SelectableValue } from '@grafana/data'; import Emoji from 'react-emoji-render'; import { FormItem, FormItemType } from 'components/GForm/GForm.types'; +import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; +import { GrafanaTeamStore } from 'models/grafana_team/grafana_team'; import { OutgoingWebhookPreset } from 'models/outgoing_webhook/outgoing_webhook.types'; import { generateAssignToTeamInputDescription } from 'utils/consts'; import { KeyValuePair } from 'utils/utils'; @@ -21,10 +24,17 @@ export const WebhookTriggerType = { Unacknowledged: new KeyValuePair('7', 'Unacknowledged'), }; -export function createForm( - presets: OutgoingWebhookPreset[] = [], - hasLabelsFeature?: boolean -): { +export function createForm({ + presets = [], + grafanaTeamStore, + alertReceiveChannelStore, + hasLabelsFeature, +}: { + presets: OutgoingWebhookPreset[]; + grafanaTeamStore: GrafanaTeamStore; + alertReceiveChannelStore: AlertReceiveChannelStore; + hasLabelsFeature?: boolean; +}): { name: string; fields: FormItem[]; } { @@ -50,7 +60,9 @@ export function createForm( )} This setting does not effect execution of the webhook.`, type: FormItemType.GSelect, extra: { - modelName: 'grafanaTeamStore', + items: grafanaTeamStore.items, + fetchItemsFn: grafanaTeamStore.updateItems, + getSearchResult: grafanaTeamStore.getSearchResult, displayField: 'name', valueField: 'id', showSearch: true, @@ -148,7 +160,10 @@ export function createForm( data.trigger_type === WebhookTriggerType.EscalationStep.key, extra: { placeholder: 'Choose (Optional)', - modelName: 'alertReceiveChannelStore', + items: alertReceiveChannelStore.items, + fetchItemsFn: alertReceiveChannelStore.fetchItems, + fetchItemFn: alertReceiveChannelStore.fetchItemById, + getSearchResult: () => AlertReceiveChannelHelper.getSearchResult(alertReceiveChannelStore), displayField: 'verbal_name', valueField: 'id', showSearch: true, diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx index 6a5b03b0..92741afb 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx @@ -93,10 +93,15 @@ export const OutgoingWebhookForm = observer((props: OutgoingWebhookFormProps) => const [selectedPreset, setSelectedPreset] = useState(undefined); const [filterValue, setFilterValue] = useState(''); - const { outgoingWebhookStore, hasFeature } = useStore(); + const { outgoingWebhookStore, hasFeature, grafanaTeamStore, alertReceiveChannelStore } = useStore(); const isNew = action === WebhookFormActionType.NEW; const isNewOrCopy = isNew || action === WebhookFormActionType.COPY; - const form = createForm(outgoingWebhookStore.outgoingWebhookPresets, hasFeature(AppFeature.Labels)); + const form = createForm({ + presets: outgoingWebhookStore.outgoingWebhookPresets, + grafanaTeamStore, + alertReceiveChannelStore, + hasLabelsFeature: hasFeature(AppFeature.Labels), + }); const handleSubmit = useCallback( async (data: Partial) => { @@ -386,75 +391,76 @@ interface WebhookTabsProps { formElement: React.ReactElement; } -const WebhookTabsContent: React.FC = ({ - id, - action, - activeTab, - data, - onHide, - onUpdate, - onDelete, - formElement, -}) => { - const [confirmationModal, setConfirmationModal] = useState(undefined); - const { outgoingWebhookStore, hasFeature } = useStore(); - const form = createForm(outgoingWebhookStore.outgoingWebhookPresets, hasFeature(AppFeature.Labels)); - return ( -
- {confirmationModal && ( - setConfirmationModal(undefined)} /> - )} +const WebhookTabsContent: React.FC = observer( + ({ id, action, activeTab, data, onHide, onUpdate, onDelete, formElement }) => { + const [confirmationModal, setConfirmationModal] = useState(undefined); + const { outgoingWebhookStore, hasFeature, grafanaTeamStore, alertReceiveChannelStore } = useStore(); + const form = createForm({ + presets: outgoingWebhookStore.outgoingWebhookPresets, + grafanaTeamStore, + alertReceiveChannelStore, + hasLabelsFeature: hasFeature(AppFeature.Labels), + }); + return ( +
+ {confirmationModal && ( + setConfirmationModal(undefined)} + /> + )} - {activeTab === WebhookTabs.Settings.key && ( - <> -
- {formElement} -
- - - - - - - - - -
-
- {data.is_legacy ? ( + {activeTab === WebhookTabs.Settings.key && ( + <>
- Legacy migrated webhooks are not editable. Make a copy to make changes. + {formElement} +
+ + + + + + + + + +
- ) : ( - '' - )} - - )} - {activeTab === WebhookTabs.LastRun.key && } -
- ); -}; + {data.is_legacy ? ( +
+ Legacy migrated webhooks are not editable. Make a copy to make changes. +
+ ) : ( + '' + )} + + )} + {activeTab === WebhookTabs.LastRun.key && } +
+ ); + } +); const WebhookPresetBlocks: React.FC<{ presets: OutgoingWebhookPreset[]; diff --git a/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.config.ts b/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.config.ts index 09f39603..88005017 100644 --- a/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.config.ts +++ b/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.config.ts @@ -1,88 +1,95 @@ import { FormItem, FormItemType } from 'components/GForm/GForm.types'; import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config'; +import { RootStore } from 'state/rootStore'; import { generateAssignToTeamInputDescription } from 'utils/consts'; const assignToTeamDescription = generateAssignToTeamInputDescription('Schedules'); -const commonFields: FormItem[] = [ - { - name: 'slack_channel_id', - label: 'Slack channel', - type: FormItemType.GSelect, - extra: { - modelName: 'slackChannelStore', - displayField: 'display_name', - showSearch: true, - allowClear: true, - nullItemName: PRIVATE_CHANNEL_NAME, +const getCommonFields = ({ slackChannelStore, userGroupStore }: RootStore): FormItem[] => + [ + { + name: 'slack_channel_id', + label: 'Slack channel', + type: FormItemType.GSelect, + extra: { + items: slackChannelStore.items, + fetchItemsFn: slackChannelStore.updateItems, + fetchItemFn: slackChannelStore.updateItem, + getSearchResult: slackChannelStore.getSearchResult, + displayField: 'display_name', + showSearch: true, + allowClear: true, + nullItemName: PRIVATE_CHANNEL_NAME, + }, + description: + 'Calendar parsing errors and notifications about the new on-call shift will be published in this channel.', }, - description: - 'Calendar parsing errors and notifications about the new on-call shift will be published in this channel.', - }, - { - name: 'user_group', - label: 'Slack user group', - type: FormItemType.GSelect, - extra: { - modelName: 'userGroupStore', - displayField: 'handle', - showSearch: true, - allowClear: true, + { + name: 'user_group', + label: 'Slack user group', + type: FormItemType.GSelect, + extra: { + items: userGroupStore.items, + fetchItemsFn: userGroupStore.updateItems, + getSearchResult: userGroupStore.getSearchResult, + displayField: 'handle', + showSearch: true, + allowClear: true, + }, + description: + 'Group members will be automatically updated with current on-call. In case you want to ping on-call with @group_name.', }, - description: - 'Group members will be automatically updated with current on-call. In case you want to ping on-call with @group_name.', - }, - { - name: 'notify_oncall_shift_freq', - label: 'Notification frequency', - type: FormItemType.RemoteSelect, - normalize: (value) => value, - extra: { - href: '/schedules/notify_oncall_shift_freq_options/', - displayField: 'display_name', - openMenuOnFocus: false, + { + name: 'notify_oncall_shift_freq', + label: 'Notification frequency', + type: FormItemType.RemoteSelect, + normalize: (value) => value, + extra: { + href: '/schedules/notify_oncall_shift_freq_options/', + displayField: 'display_name', + openMenuOnFocus: false, + }, + description: 'Specify the frequency that shift notifications are sent to scheduled team members.', }, - description: 'Specify the frequency that shift notifications are sent to scheduled team members.', - }, - { - name: 'notify_empty_oncall', - label: 'Action for slot when no one is on-call', - type: FormItemType.RemoteSelect, - normalize: (value) => value, - extra: { - href: '/schedules/notify_empty_oncall_options/', - displayField: 'display_name', - openMenuOnFocus: false, + { + name: 'notify_empty_oncall', + label: 'Action for slot when no one is on-call', + type: FormItemType.RemoteSelect, + normalize: (value) => value, + extra: { + href: '/schedules/notify_empty_oncall_options/', + displayField: 'display_name', + openMenuOnFocus: false, + }, + description: 'Specify how to notify team members when there is no one scheduled for an on-call shift.', }, - description: 'Specify how to notify team members when there is no one scheduled for an on-call shift.', - }, - { - name: 'mention_oncall_start', - label: 'Current shift notification settings', - type: FormItemType.RemoteSelect, - normalize: (value) => value, - extra: { - href: '/schedules/mention_options/', - displayField: 'display_name', - openMenuOnFocus: false, + { + name: 'mention_oncall_start', + label: 'Current shift notification settings', + type: FormItemType.RemoteSelect, + normalize: (value) => value, + extra: { + href: '/schedules/mention_options/', + displayField: 'display_name', + openMenuOnFocus: false, + }, + description: 'Specify how to notify a team member when their on-call shift begins ', }, - description: 'Specify how to notify a team member when their on-call shift begins ', - }, - { - name: 'mention_oncall_next', - label: 'Next shift notification settings', - type: FormItemType.RemoteSelect, - normalize: (value) => value, - extra: { - href: '/schedules/mention_options/', - displayField: 'display_name', - openMenuOnFocus: false, + { + name: 'mention_oncall_next', + label: 'Next shift notification settings', + type: FormItemType.RemoteSelect, + normalize: (value) => value, + extra: { + href: '/schedules/mention_options/', + displayField: 'display_name', + openMenuOnFocus: false, + }, + description: 'Specify how to notify a team member when their shift is the next one scheduled', }, - description: 'Specify how to notify a team member when their shift is the next one scheduled', - }, -].map((field) => ({ ...field, collapsed: true })); + ].map((field) => ({ ...field, collapsed: true })); -export const iCalForm: { name: string; fields: FormItem[] } = { +export const getICalForm = (rootStore: RootStore) => ({ name: 'Schedule', fields: [ { @@ -96,7 +103,9 @@ export const iCalForm: { name: string; fields: FormItem[] } = { description: assignToTeamDescription, type: FormItemType.GSelect, extra: { - modelName: 'grafanaTeamStore', + items: rootStore.grafanaTeamStore.items, + fetchItemsFn: rootStore.grafanaTeamStore.updateItems, + getSearchResult: rootStore.grafanaTeamStore.getSearchResult, displayField: 'name', valueField: 'id', showSearch: true, @@ -117,11 +126,11 @@ export const iCalForm: { name: string; fields: FormItem[] } = { extra: { rows: 2 }, }, - ...commonFields, + ...getCommonFields(rootStore), ], -}; +}); -export const calendarForm: { name: string; fields: FormItem[] } = { +export const getCalendarForm = (rootStore: RootStore) => ({ name: 'Schedule', fields: [ { @@ -135,7 +144,9 @@ export const calendarForm: { name: string; fields: FormItem[] } = { description: assignToTeamDescription, type: FormItemType.GSelect, extra: { - modelName: 'grafanaTeamStore', + items: rootStore.grafanaTeamStore.items, + fetchItemsFn: rootStore.grafanaTeamStore.updateItems, + getSearchResult: rootStore.grafanaTeamStore.getSearchResult, displayField: 'name', valueField: 'id', showSearch: true, @@ -157,11 +168,11 @@ export const calendarForm: { name: string; fields: FormItem[] } = { extra: { rows: 2 }, }, - ...commonFields, + ...getCommonFields(rootStore), ], -}; +}); -export const apiForm: { name: string; fields: FormItem[] } = { +export const getApiForm = (rootStore: RootStore) => ({ name: 'Schedule', fields: [ { @@ -175,13 +186,15 @@ export const apiForm: { name: string; fields: FormItem[] } = { description: assignToTeamDescription, type: FormItemType.GSelect, extra: { - modelName: 'grafanaTeamStore', + items: rootStore.grafanaTeamStore.items, + fetchItemsFn: rootStore.grafanaTeamStore.updateItems, + getSearchResult: rootStore.grafanaTeamStore.getSearchResult, displayField: 'name', valueField: 'id', showSearch: true, allowClear: true, }, }, - ...commonFields, + ...getCommonFields(rootStore), ], -}; +}); diff --git a/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx b/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx index f85d0be5..b34c0172 100644 --- a/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx +++ b/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx @@ -11,7 +11,7 @@ import { useStore } from 'state/useStore'; import { UserActions } from 'utils/authorization/authorization'; import { openWarningNotification } from 'utils/utils'; -import { apiForm, calendarForm, iCalForm } from './ScheduleForm.config'; +import { getApiForm, getCalendarForm, getICalForm } from './ScheduleForm.config'; import { prepareForEdit } from './ScheduleForm.helpers'; import styles from './ScheduleForm.module.css'; @@ -25,12 +25,6 @@ interface ScheduleFormProps { type?: ScheduleType; } -const scheduleTypeToForm = { - [ScheduleType.Calendar]: calendarForm, - [ScheduleType.Ical]: iCalForm, - [ScheduleType.API]: apiForm, -}; - export const ScheduleForm = observer((props: ScheduleFormProps) => { const { id, type, onSubmit, onHide } = props; const isNew = id === 'new'; @@ -39,6 +33,15 @@ export const ScheduleForm = observer((props: ScheduleFormProps) => { const { scheduleStore, userStore } = store; + const scheduleTypeToForm = useMemo( + () => ({ + [ScheduleType.Calendar]: getCalendarForm(store), + [ScheduleType.Ical]: getICalForm(store), + [ScheduleType.API]: getApiForm(store), + }), + [] + ); + const data = useMemo(() => { return isNew ? { team: userStore.currentUser?.current_team, type } : prepareForEdit(scheduleStore.items[id]); }, [id]); diff --git a/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx b/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx index 55ca4a40..e073543b 100644 --- a/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx +++ b/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx @@ -5,9 +5,10 @@ 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 { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; import { Alert } from 'models/alertgroup/alertgroup.types'; import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config'; import { useStore } from 'state/useStore'; import { useDebouncedCallback } from 'utils/hooks'; @@ -23,8 +24,8 @@ interface TemplatePreviewProps { templateBody: string | null; templateType?: 'plain' | 'html' | 'image' | 'boolean'; templateIsRoute?: boolean; - payload?: JSON; - alertReceiveChannelId: AlertReceiveChannel['id']; + payload?: { [key: string]: unknown }; + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']; alertGroupId?: Alert['pk']; outgoingWebhookId?: OutgoingWebhook['id']; templatePage: TEMPLATE_PAGE; @@ -58,14 +59,14 @@ export const TemplatePreview = observer((props: TemplatePreviewProps) => { const [conditionalResult, setConditionalResult] = useState({}); const store = useStore(); - const { alertReceiveChannelStore, alertGroupStore, outgoingWebhookStore } = store; + const { alertGroupStore, outgoingWebhookStore } = store; const handleTemplateBodyChange = useDebouncedCallback(() => { (templatePage === TEMPLATE_PAGE.Webhooks ? outgoingWebhookStore.renderPreview(outgoingWebhookId, templateName, templateBody, payload) : alertGroupId ? alertGroupStore.renderPreview(alertGroupId, templateName, templateBody) - : alertReceiveChannelStore.renderPreview(alertReceiveChannelId, templateName, templateBody, payload) + : AlertReceiveChannelHelper.renderPreview(alertReceiveChannelId, templateName, templateBody, payload) ) .then((data) => { setResult(data); diff --git a/grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx b/grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx index 7029608b..161f1f7d 100644 --- a/grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx +++ b/grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx @@ -8,19 +8,19 @@ import { Block } from 'components/GBlock/Block'; import { Text } from 'components/Text/Text'; import styles from 'containers/IntegrationTemplate/IntegrationTemplate.module.scss'; import { TemplatePreview, TEMPLATE_PAGE } from 'containers/TemplatePreview/TemplatePreview'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; const cx = cn.bind(styles); interface ResultProps { - alertReceiveChannelId?: AlertReceiveChannel['id']; + alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id']; outgoingWebhookId?: OutgoingWebhook['id']; templateBody: string; template: TemplateForEdit; isAlertGroupExisting?: boolean; chatOpsPermalink?: string; - payload?: JSON; + payload?: { [key: string]: unknown }; error?: string; onSaveAndFollowLink?: (link: string) => void; templateIsRoute?: boolean; diff --git a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx index fca418f4..8e9a1915 100644 --- a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx +++ b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx @@ -8,10 +8,10 @@ import { MonacoEditor, MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEdi import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.config'; import { Text } from 'components/Text/Text'; import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates'; import { Alert } from 'models/alertgroup/alertgroup.types'; import { OutgoingWebhook, OutgoingWebhookResponse } from 'models/outgoing_webhook/outgoing_webhook.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { useStore } from 'state/useStore'; import styles from './TemplatesAlertGroupsList.module.css'; @@ -26,7 +26,7 @@ export enum TEMPLATE_PAGE { interface TemplatesAlertGroupsListProps { templatePage: TEMPLATE_PAGE; templates: AlertTemplatesDTO[]; - alertReceiveChannelId?: AlertReceiveChannel['id']; + alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id']; outgoingwebhookId?: OutgoingWebhook['id']; heading?: string; diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.helpers.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.helpers.ts index 6810da09..fef73db5 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.helpers.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.helpers.ts @@ -1,11 +1,186 @@ -import { AlertReceiveChannel } from './alert_receive_channel.types'; +import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; +import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; +import { makeRequest } from 'network/network'; +import { ApiSchemas } from 'network/oncall-api/api.types'; +import { onCallApi } from 'network/oncall-api/http-client'; +import { SelectOption } from 'state/types'; +import { showApiError } from 'utils/utils'; -export function getAlertReceiveChannelDisplayName(alertReceiveChannel?: AlertReceiveChannel, withDescription = true) { - if (!alertReceiveChannel) { - return ''; +import { AlertReceiveChannelStore } from './alert_receive_channel'; +import { MaintenanceMode } from './alert_receive_channel.types'; + +export class AlertReceiveChannelHelper { + static getAlertReceiveChannelDisplayName( + alertReceiveChannel?: ApiSchemas['AlertReceiveChannel'], + withDescription = true + ) { + if (!alertReceiveChannel) { + return ''; + } + + return withDescription && alertReceiveChannel.description + ? `${alertReceiveChannel.verbal_name} (${alertReceiveChannel.description})` + : alertReceiveChannel.verbal_name; } - return withDescription && alertReceiveChannel.description - ? `${alertReceiveChannel.verbal_name} (${alertReceiveChannel.description})` - : alertReceiveChannel.verbal_name; + static getSearchResult(store: AlertReceiveChannelStore) { + return store.searchResult + ? store.searchResult.map( + (alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']) => store.items?.[alertReceiveChannelId] + ) + : undefined; + } + + static getPaginatedSearchResult(store: AlertReceiveChannelStore) { + return store.paginatedSearchResult + ? { + page_size: store.paginatedSearchResult.page_size, + count: store.paginatedSearchResult.count, + results: store.paginatedSearchResult.results?.map( + (alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']) => store.items?.[alertReceiveChannelId] + ), + } + : undefined; + } + + static getIntegration( + store: AlertReceiveChannelStore, + alertReceiveChannel: Partial + ): SelectOption { + return ( + store.alertReceiveChannelOptions && + alertReceiveChannel && + store.alertReceiveChannelOptions.find( + (alertReceiveChannelOption: SelectOption) => alertReceiveChannelOption.value === alertReceiveChannel.integration + ) + ); + } + + static async deleteAlertReceiveChannel(id: ApiSchemas['AlertReceiveChannel']['id']) { + return (await onCallApi().DELETE('/alert_receive_channels/{id}/', { params: { path: { id } } })).data; + } + + static async getGrafanaAlertingContactPoints() { + return (await onCallApi().GET('/alert_receive_channels/contact_points/', undefined)).data; + } + + static async connectContactPoint( + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], + datasource_uid: string, + contact_point_name: string + ) { + return ( + await onCallApi().POST('/alert_receive_channels/{id}/connect_contact_point/', { + params: { path: { id: alertReceiveChannelId } }, + body: { + datasource_uid, + contact_point_name, + }, + }) + ).data; + } + + static async disconnectContactPoint( + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], + datasource_uid: string, + contact_point_name: string + ) { + return ( + await onCallApi().POST('/alert_receive_channels/{id}/disconnect_contact_point/', { + params: { path: { id: alertReceiveChannelId } }, + body: { + datasource_uid, + contact_point_name, + }, + }) + ).data; + } + + static async createContactPoint( + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], + datasource_uid: string, + contact_point_name: string + ) { + return ( + await onCallApi().POST('/alert_receive_channels/{id}/create_contact_point/', { + params: { path: { id: alertReceiveChannelId } }, + body: { + datasource_uid, + contact_point_name, + }, + }) + ).data; + } + + static async sendDemoAlert(id: ApiSchemas['AlertReceiveChannel']['id'], payload?: { [key: string]: unknown }) { + await onCallApi().POST('/alert_receive_channels/{id}/send_demo_alert/', { + params: { path: { id } }, + body: { demo_alert_payload: payload }, + }); + } + + static async renderPreview( + id: ApiSchemas['AlertReceiveChannel']['id'], + template_name: string, + template_body: string, + payload: { [key: string]: unknown } + ) { + return ( + await onCallApi().POST('/alertgroups/{id}/preview_template/', { + params: { path: { id } }, + body: { template_name, template_body, payload }, + }) + ).data; + } + + static async changeTeam(id: ApiSchemas['AlertReceiveChannel']['id'], teamId: GrafanaTeam['id']) { + return ( + await onCallApi().PUT('/alert_receive_channels/{id}/change_team/', { + params: { path: { id }, query: { team_id: String(teamId) } }, + }) + ).data; + } + + static async migrateChannel(id: ApiSchemas['AlertReceiveChannel']['id']) { + return (await onCallApi().POST('/alert_receive_channels/{id}/migrate/', { params: { path: { id } } })).data; + } + + static async startMaintenanceMode( + id: ApiSchemas['AlertReceiveChannel']['id'], + mode: MaintenanceMode, + duration: ApiSchemas['DurationEnum'] + ) { + return ( + await onCallApi().POST('/alert_receive_channels/{id}/start_maintenance/', { + params: { path: { id } }, + body: { + mode, + duration, + }, + }) + ).data; + } + + static async stopMaintenanceMode(id: ApiSchemas['AlertReceiveChannel']['id']) { + return (await onCallApi().POST('/alert_receive_channels/{id}/stop_maintenance/', { params: { path: { id } } })) + .data; + } + + static async sendDemoAlertToParticularRoute(id: ChannelFilter['id']) { + await makeRequest(`/channel_filters/${id}/send_demo_alert/`, { method: 'POST' }).catch(showApiError); + } + + static async convertRegexpTemplateToJinja2Template(id: ChannelFilter['id']) { + const result = await makeRequest(`/channel_filters/${id}/convert_from_regex_to_jinja2/`, { method: 'POST' }).catch( + showApiError + ); + return result; + } + + static async createChannelFilter(data: Partial) { + return await makeRequest('/channel_filters/', { + method: 'POST', + data, + }); + } } diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index 891b4aff..cc431298 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -1,125 +1,110 @@ import { omit } from 'lodash-es'; -import { action, observable, makeObservable, runInAction } from 'mobx'; +import { runInAction, makeAutoObservable } from 'mobx'; import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates'; import { Alert } from 'models/alertgroup/alertgroup.types'; -import { BaseStore } from 'models/base_store'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; -import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; import { Heartbeat } from 'models/heartbeat/heartbeat.types'; import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; import { makeRequest } from 'network/network'; +import { ApiSchemas } from 'network/oncall-api/api.types'; +import { operations } from 'network/oncall-api/autogenerated-api.types'; +import { onCallApi } from 'network/oncall-api/http-client'; import { move } from 'state/helpers'; -import { RootStore } from 'state/rootStore'; -import { SelectOption } from 'state/types'; +import { RootBaseStore } from 'state/rootBaseStore/RootBaseStore'; import { WithGlobalNotification } from 'utils/decorators'; -import { showApiError } from 'utils/utils'; +import { OmitReadonlyMembers } from 'utils/types'; -import { - AlertReceiveChannel, - AlertReceiveChannelOption, - AlertReceiveChannelCounters, - ContactPoint, - MaintenanceMode, - SupportedIntegrationFilters, -} from './alert_receive_channel.types'; +import { AlertReceiveChannelCounters, ContactPoint } from './alert_receive_channel.types'; -export class AlertReceiveChannelStore extends BaseStore { - @observable.shallow - searchResult: Array; - - @observable.shallow - paginatedSearchResult: { count?: number; results?: Array; page_size?: number } = {}; - - @observable.shallow - items: { [id: string]: AlertReceiveChannel } = {}; - - @observable.shallow +export class AlertReceiveChannelStore { + path = '/alert_receive_channels/'; + rootStore: RootBaseStore; + searchResult: Array; + paginatedSearchResult: { + count?: number; + results?: Array; + page_size?: number; + } = {}; + items: { + [id: string]: ApiSchemas['AlertReceiveChannel']; + } = {}; counters: { [id: string]: AlertReceiveChannelCounters } = {}; - - @observable channelFilterIds: { [id: string]: Array } = {}; - - @observable.shallow channelFilters: { [id: string]: ChannelFilter } = {}; - - @observable alertReceiveChannelToHeartbeat: { [id: string]: Heartbeat['id']; } = {}; - - @observable.shallow actions: { [id: string]: OutgoingWebhook[] } = {}; - - @observable.shallow - alertReceiveChannelOptions: AlertReceiveChannelOption[] = []; - - @observable.shallow + alertReceiveChannelOptions: Array = []; templates: { [id: string]: AlertTemplatesDTO[] } = {}; - - @observable connectedContactPoints: { [id: string]: ContactPoint[] } = {}; - constructor(rootStore: RootStore) { - super(rootStore); - - makeObservable(this); - - this.path = '/alert_receive_channels/'; + constructor(rootStore: RootBaseStore) { + makeAutoObservable(this, undefined, { autoBind: true }); + this.rootStore = rootStore; } - getSearchResult(_query = '') { - if (!this.searchResult) { - return undefined; - } - - return this.searchResult.map( - (alertReceiveChannelId: AlertReceiveChannel['id']) => this.items?.[alertReceiveChannelId] - ); + @WithGlobalNotification({ failure: 'There was an issue creating Integration. Please try again.' }) + async create({ data, skipErrorHandling }: { data: ApiSchemas['AlertReceiveChannel']; skipErrorHandling?: boolean }) { + const result = await onCallApi({ skipErrorHandling }).POST('/alert_receive_channels/', { + params: {}, + body: data, + }); + await this.rootStore.organizationStore.loadCurrentOrganization(); + return result.data; } - getPaginatedSearchResult(_query = '') { - if (!this.paginatedSearchResult) { - return undefined; - } - - return { - page_size: this.paginatedSearchResult.page_size, - count: this.paginatedSearchResult.count, - results: this.paginatedSearchResult.results?.map( - (alertReceiveChannelId: AlertReceiveChannel['id']) => this.items?.[alertReceiveChannelId] - ), - }; + @WithGlobalNotification({ failure: 'There was an issue updating Integration. Please try again.' }) + async update({ + id, + data, + skipErrorHandling, + }: { + id: ApiSchemas['AlertReceiveChannelUpdate']['id']; + data: ApiSchemas['AlertReceiveChannelUpdate']; + skipErrorHandling?: boolean; + }) { + const result = await onCallApi({ skipErrorHandling }).PUT('/alert_receive_channels/{id}/', { + params: { path: { id } }, + body: data, + }); + await this.rootStore.organizationStore.loadCurrentOrganization(); + return result.data; } - @action - async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling = false): Promise { - const alertReceiveChannel = await this.getById(id, skipErrorHandling); + async fetchItemById( + id: ApiSchemas['AlertReceiveChannel']['id'], + skipErrorHandling = false + ): Promise { + const alertReceiveChannel = await onCallApi({ skipErrorHandling }).GET('/alert_receive_channels/{id}/', { + params: { path: { id } }, + }); runInAction(() => { - // @ts-ignore this.items = { ...this.items, - [id]: omit(alertReceiveChannel, 'heartbeat'), + [id]: { ...alertReceiveChannel.data, heartbeat: alertReceiveChannel.data.heartbeat || null }, }; }); - this.populateHearbeats([alertReceiveChannel]); + this.populateHearbeats([alertReceiveChannel.data]); - return alertReceiveChannel; + return alertReceiveChannel.data; } - @action - async updateItems(query: any = '') { + async fetchItems(query: any = '') { const params = typeof query === 'string' ? { search: query } : query; - const { results } = await makeRequest(this.path, { params }); + const { + data: { results }, + } = await onCallApi().GET('/alert_receive_channels/', { params }); runInAction(() => { this.items = { ...this.items, ...results.reduce( - (acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({ + (acc: { [key: number]: ApiSchemas['AlertReceiveChannel'] }, item: ApiSchemas['AlertReceiveChannel']) => ({ ...acc, [item.id]: omit(item, 'heartbeat'), }), @@ -131,26 +116,28 @@ export class AlertReceiveChannelStore extends BaseStore { this.populateHearbeats(results); runInAction(() => { - this.searchResult = results.map((item: AlertReceiveChannel) => item.id); + this.searchResult = results.map((item: ApiSchemas['AlertReceiveChannel']) => item.id); }); - this.updateCounters(); + this.fetchCounters(); return results; } - async updatePaginatedItems({ + async fetchPaginatedItems({ filters, page = 1, - updateCounters = false, + shouldFetchCounters = false, invalidateFn = undefined, }: { - filters: SupportedIntegrationFilters; + filters: operations['alert_receive_channels_list']['parameters']['query']; page: number; - updateCounters: boolean; + shouldFetchCounters: boolean; invalidateFn: () => boolean; }) { - const { count, results, page_size } = await makeRequest(this.path, { params: { ...filters, page } }); + const { + data: { count, results, page_size }, + } = await onCallApi().GET('/alert_receive_channels/', { params: { query: { ...filters, page } } }); if (invalidateFn?.()) { return undefined; @@ -160,7 +147,7 @@ export class AlertReceiveChannelStore extends BaseStore { this.items = { ...this.items, ...results.reduce( - (acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({ + (acc: { [key: number]: ApiSchemas['AlertReceiveChannel'] }, item: ApiSchemas['AlertReceiveChannel']) => ({ ...acc, [item.id]: omit(item, 'heartbeat'), }), @@ -174,26 +161,29 @@ export class AlertReceiveChannelStore extends BaseStore { runInAction(() => { this.paginatedSearchResult = { count, - results: results.map((item: AlertReceiveChannel) => item.id), + results: results.map((item: ApiSchemas['AlertReceiveChannel']) => item.id), page_size, }; }); - if (updateCounters) { - this.updateCounters(); + if (shouldFetchCounters) { + this.fetchCounters(); } return results; } - populateHearbeats(alertReceiveChannels: AlertReceiveChannel[]) { - const heartbeats = alertReceiveChannels.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => { - if (alertReceiveChannel.heartbeat) { - acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat; - } + populateHearbeats(alertReceiveChannels: Array) { + const heartbeats = alertReceiveChannels.reduce( + (acc: any, alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) => { + if (alertReceiveChannel.heartbeat) { + acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat; + } - return acc; - }, {}); + return acc; + }, + {} + ); runInAction(() => { this.rootStore.heartbeatStore.items = { @@ -203,7 +193,7 @@ export class AlertReceiveChannelStore extends BaseStore { }); const alertReceiveChannelToHeartbeat = alertReceiveChannels.reduce( - (acc: any, alertReceiveChannel: AlertReceiveChannel) => { + (acc: any, alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) => { if (alertReceiveChannel.heartbeat) { acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id; } @@ -221,8 +211,7 @@ export class AlertReceiveChannelStore extends BaseStore { }); } - @action - async updateChannelFilters(alertReceiveChannelId: AlertReceiveChannel['id'], isOverwrite = false) { + async fetchChannelFilters(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], isOverwrite = false) { const response = await makeRequest(`/channel_filters/`, { params: { alert_receive_channel: alertReceiveChannelId }, }); @@ -259,32 +248,6 @@ export class AlertReceiveChannelStore extends BaseStore { }); } - @action - async updateChannelFilter(channelFilterId: ChannelFilter['id']) { - const response = await makeRequest(`/channel_filters/${channelFilterId}/`, {}); - - runInAction(() => { - this.channelFilters = { - ...this.channelFilters, - [channelFilterId]: response, - }; - }); - } - - async migrateChannel(id: AlertReceiveChannel['id']) { - return await makeRequest(`/alert_receive_channels/${id}/migrate`, { - method: 'POST', - }); - } - - async createChannelFilter(data: Partial) { - return await makeRequest('/channel_filters/', { - method: 'POST', - data, - }); - } - - @action async saveChannelFilter(channelFilterId: ChannelFilter['id'], data: Partial) { const response = await makeRequest(`/channel_filters/${channelFilterId}/`, { method: 'PUT', @@ -301,9 +264,8 @@ export class AlertReceiveChannelStore extends BaseStore { return response; } - @action async moveChannelFilterToPosition( - alertReceiveChannelId: AlertReceiveChannel['id'], + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], oldIndex: number, newIndex: number ) { @@ -317,10 +279,9 @@ export class AlertReceiveChannelStore extends BaseStore { await makeRequest(`/channel_filters/${channelFilterId}/move_to_position/?position=${newIndex}`, { method: 'PUT' }); - this.updateChannelFilters(alertReceiveChannelId, true); + this.fetchChannelFilters(alertReceiveChannelId, true); } - @action async deleteChannelFilter(channelFilterId: ChannelFilter['id']) { const channelFilter = this.channelFilters[channelFilterId]; @@ -333,47 +294,43 @@ export class AlertReceiveChannelStore extends BaseStore { method: 'DELETE', }); - return this.updateChannelFilters(channelFilter.alert_receive_channel, true); + return this.fetchChannelFilters(channelFilter.alert_receive_channel, true); } - @action.bound - async updateAlertReceiveChannelOptions() { - const response = await makeRequest(`/alert_receive_channels/integration_options/`, {}); + async fetchAlertReceiveChannelOptions() { + const { data } = await onCallApi().GET(`/alert_receive_channels/integration_options/`, undefined); runInAction(() => { - this.alertReceiveChannelOptions = response; + this.alertReceiveChannelOptions = data; }); } - getIntegration(alertReceiveChannel: Partial): SelectOption { - return ( - this.alertReceiveChannelOptions && - alertReceiveChannel && - this.alertReceiveChannelOptions.find( - (alertReceiveChannelOption: SelectOption) => alertReceiveChannelOption.value === alertReceiveChannel.integration - ) - ); - } - - @action.bound @WithGlobalNotification({ success: 'Integration has been saved', failure: 'Failed to save integration' }) - async saveAlertReceiveChannel(id: AlertReceiveChannel['id'], data: Partial) { - const item = await this.update(id, data, undefined, true); + async saveAlertReceiveChannel( + id: ApiSchemas['AlertReceiveChannel']['id'], + payload: OmitReadonlyMembers + ) { + const currentIntegration = this.items[id]; + const { data } = await onCallApi().PUT('/alert_receive_channels/{id}/', { + params: { path: { id } }, + body: { + description_short: currentIntegration.description_short, + verbal_name: currentIntegration.verbal_name, + allow_source_based_resolving: currentIntegration.allow_source_based_resolving, + alert_group_labels: currentIntegration.alert_group_labels, + ...payload, + } as ApiSchemas['AlertReceiveChannelUpdate'], + }); runInAction(() => { this.items = { ...this.items, - [id]: item, + [id]: data, }; }); } - async deleteAlertReceiveChannel(id: AlertReceiveChannel['id']) { - return await this.delete(id); - } - - @action - async updateTemplates(alertReceiveChannelId: AlertReceiveChannel['id'], alertGroupId?: Alert['pk']) { + async fetchTemplates(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], alertGroupId?: Alert['pk']) { const response = await makeRequest(`/alert_receive_channel_templates/${alertReceiveChannelId}/`, { params: { alert_group_id: alertGroupId }, withCredentials: true, @@ -387,20 +344,10 @@ export class AlertReceiveChannelStore extends BaseStore { }); } - @action - async updateItem(id: AlertReceiveChannel['id']) { - const item = await this.getById(id); - - runInAction(() => { - this.items = { - ...this.items, - [id]: item, - }; - }); - } - - @action - async saveTemplates(alertReceiveChannelId: AlertReceiveChannel['id'], data: Partial) { + async saveTemplates( + alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], + data: Partial + ) { const response = await makeRequest(`/alert_receive_channel_templates/${alertReceiveChannelId}/`, { method: 'PUT', data, @@ -415,26 +362,23 @@ export class AlertReceiveChannelStore extends BaseStore { }); } - async getGrafanaAlertingContactPoints() { - return await makeRequest(`${this.path}contact_points/`, {}).catch(showApiError); - } - - @action - async updateConnectedContactPoints(alertReceiveChannelId: AlertReceiveChannel['id']) { - const response = await makeRequest(`${this.path}${alertReceiveChannelId}/connected_contact_points `, {}); + async fetchConnectedContactPoints(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']) { + const { data } = await onCallApi().GET('/alert_receive_channels/{id}/connected_contact_points/', { + params: { path: { id: alertReceiveChannelId } }, + }); runInAction(() => { this.connectedContactPoints = { ...this.connectedContactPoints, - [alertReceiveChannelId]: response.reduce((list: ContactPoint[], payload) => { - payload.contact_points.forEach((contactPoint: { name: string; notification_connected: boolean }) => { + [alertReceiveChannelId]: data.reduce((list: ContactPoint[], payload) => { + payload.contact_points.forEach((contactPoint) => { list.push({ dataSourceName: payload.name, dataSourceId: payload.uid, contactPoint: contactPoint.name, notificationConnected: contactPoint.notification_connected, - } as ContactPoint); + }); }); return list; @@ -443,140 +387,25 @@ export class AlertReceiveChannelStore extends BaseStore { }); } - async connectContactPoint( - alertReceiveChannelId: AlertReceiveChannel['id'], - datasource_uid: string, - contact_point_name: string - ) { - return await makeRequest(`${this.path}${alertReceiveChannelId}/connect_contact_point`, { - method: 'POST', - data: { - datasource_uid, - contact_point_name, - }, - }); - } - - async disconnectContactPoint( - alertReceiveChannelId: AlertReceiveChannel['id'], - datasource_uid: string, - contact_point_name: string - ) { - return await makeRequest(`${this.path}${alertReceiveChannelId}/disconnect_contact_point`, { - method: 'POST', - data: { - datasource_uid, - contact_point_name, - }, - }); - } - - async createContactPoint( - alertReceiveChannelId: AlertReceiveChannel['id'], - datasource_uid: string, - contact_point_name: string - ) { - return await makeRequest(`${this.path}${alertReceiveChannelId}/create_contact_point`, { - method: 'POST', - data: { - datasource_uid, - contact_point_name, - }, - }); - } - - async getAccessLogs(alertReceiveChannelId: AlertReceiveChannel['id']) { - const { integration_log } = await makeRequest(`/alert_receive_channel_access_log/${alertReceiveChannelId}/`, {}); - - return integration_log; - } - - async sendDemoAlert(id: AlertReceiveChannel['id'], payload: string = undefined) { - const requestConfig: any = { - method: 'POST', - }; - - if (payload) { - requestConfig.data = { - demo_alert_payload: payload, - }; - } - - await makeRequest(`${this.path}${id}/send_demo_alert/`, requestConfig).catch(showApiError); - } - - async sendDemoAlertToParticularRoute(id: ChannelFilter['id']) { - await makeRequest(`/channel_filters/${id}/send_demo_alert/`, { method: 'POST' }).catch(showApiError); - } - - async convertRegexpTemplateToJinja2Template(id: ChannelFilter['id']) { - const result = await makeRequest(`/channel_filters/${id}/convert_from_regex_to_jinja2/`, { method: 'POST' }).catch( - showApiError - ); - return result; - } - - async renderPreview(id: AlertReceiveChannel['id'], template_name: string, template_body: string, payload: JSON) { - return await makeRequest(`${this.path}${id}/preview_template/`, { - method: 'POST', - data: { template_name, template_body, payload }, - }); - } - - async changeTeam(id: AlertReceiveChannel['id'], teamId: GrafanaTeam['id']) { - return await makeRequest(`${this.path}${id}/change_team`, { - params: { team_id: String(teamId) }, - method: 'PUT', - }); - } - - @action - async updateCounters() { - const counters = await makeRequest(`${this.path}counters`, { - method: 'GET', - }); - + async fetchCounters() { + const { data } = await onCallApi().GET('/alert_receive_channels/counters/', undefined); runInAction(() => { - this.counters = counters; + this.counters = data; }); } - @action - async updateCountersForIntegration(id: AlertReceiveChannel['id']): Promise { - const counters = await makeRequest(`${this.path}${id}/counters`, { - method: 'GET', - }); + async fetchCountersForIntegration(id: ApiSchemas['AlertReceiveChannel']['id']) { + const { data } = await onCallApi().GET('/alert_receive_channels/{id}/counters/', { params: { path: { id } } }); runInAction(() => { this.counters = { ...this.counters, [id]: { - ...counters[id], + ...data[id], }, }; }); - return counters; + return data; } - - startMaintenanceMode = (id: AlertReceiveChannel['id'], mode: MaintenanceMode, duration: number): Promise => - makeRequest(`${this.path}${id}/start_maintenance/`, { - method: 'POST', - data: { - mode, - duration, - }, - }); - - stopMaintenanceMode = (id: AlertReceiveChannel['id']) => - makeRequest(`${this.path}${id}/stop_maintenance/`, { - method: 'POST', - }); - - addLabel = (id: AlertReceiveChannel['id'], data) => { - makeRequest(`${this.path}${id}/associate_label`, { - method: 'POST', - data, - }); - }; } diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts index 3746921c..07690b9c 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts @@ -1,66 +1,12 @@ -import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; -import { Heartbeat } from 'models/heartbeat/heartbeat.types'; -import { LabelKeyValue } from 'models/label/label.types'; -import { User } from 'models/user/user.types'; +import { operations } from 'network/oncall-api/autogenerated-api.types'; export enum MaintenanceMode { Debug = 0, Maintenance = 1, } -export interface AlertReceiveChannelOption { - display_name: string; - value: string; - featured: boolean; - short_description: string; - featured_tag_name: string; -} - -export interface AlertReceiveChannelCounters { - alerts_count: number; - alert_groups_count: number; -} - -export interface AlertReceiveChannel { - id: string; - integration: string; - smile_code: string; - verbal_name: string; - description: string; - description_short: string; - author: User['pk']; - team: GrafanaTeam['id']; - created_at: string; - integration_url: string; - inbound_email: string; - allow_source_based_resolving: boolean; - is_able_to_autoresolve: boolean; - is_based_on_alertmanager: boolean; - default_channel_filter: number; - instructions: string; - demo_alert_enabled: boolean; - demo_alert_payload: any; - maintenance_mode?: MaintenanceMode; - maintenance_till?: number; - heartbeat: Heartbeat | null; - is_available_for_integration_heartbeat: boolean; - routes_count: number; - connected_escalations_chains_count: number; - allow_delete: boolean; - deleted?: boolean; - labels: LabelKeyValue[]; - alert_group_labels: { - inheritable: Record; - custom: LabelKeyValue[]; - template: string; - }; - alertmanager_v2_migrated_at?: string | null; -} - -export interface AlertReceiveChannelChoice { - display_name: string; - value: number; -} +export type AlertReceiveChannelCounters = + operations['alert_receive_channels_counters_retrieve']['responses']['200']['content']['application/json'][string]; export interface ContactPoint { dataSourceName: string; @@ -68,11 +14,3 @@ export interface ContactPoint { contactPoint: string; notificationConnected: boolean; } - -export interface SupportedIntegrationFilters { - integration?: string[]; - integration_ne?: string[]; - team?: string[]; - label?: string[]; - searchTerm?: string; -} diff --git a/grafana-plugin/src/models/alert_receive_channel_filters/alert_receive_channel_filters.ts b/grafana-plugin/src/models/alert_receive_channel_filters/alert_receive_channel_filters.ts index 19b19580..84cc96ff 100644 --- a/grafana-plugin/src/models/alert_receive_channel_filters/alert_receive_channel_filters.ts +++ b/grafana-plugin/src/models/alert_receive_channel_filters/alert_receive_channel_filters.ts @@ -20,15 +20,15 @@ export class AlertReceiveChannelFiltersStore extends BaseStore { this.path = '/alert_receive_channels/'; } - getSearchResult() { + getSearchResult = () => { if (!this.searchResult) { return undefined; } return this.searchResult.map((value: SelectOption['value']) => this.items?.[value]); - } + }; - @action + @action.bound async updateItems(query = '') { const results = await makeRequest(`${this.path}`, { params: { search: query, filters: true }, diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 28e54e99..c0b56950 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -1,7 +1,6 @@ import { action, observable, makeObservable, runInAction } from 'mobx'; import qs from 'query-string'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { BaseStore } from 'models/base_store'; import { ActionKey } from 'models/loader/action-keys'; import { User } from 'models/user/user.types'; @@ -97,27 +96,58 @@ export class AlertGroupStore extends BaseStore { }).catch(showApiError); } - @action + @action.bound async updateItem(id: Alert['pk']) { const item = await this.getById(id); runInAction(() => { this.items = { ...this.items, - [item.id]: item, + [item.pk]: item, }; }); } - getSearchResult(query = '') { + @action.bound + async fetchItems(query = '', params = {}) { + const { results } = await makeRequest(`${this.path}`, { + params: { search: query, ...params }, + }); + + runInAction(() => { + this.items = { + ...this.items, + ...results.reduce( + (acc: { [key: number]: Alert }, item: Alert) => ({ + ...acc, + [item.pk]: item, + }), + {} + ), + }; + + this.searchResult = { + ...this.searchResult, + [query]: results.map((item: Alert) => item.pk), + }; + }); + } + + @action.bound + async fetchItemsAvailableForAttachment(query: string) { + await this.fetchItems(query, { + status: [IncidentStatus.Acknowledged, IncidentStatus.Firing, IncidentStatus.Silenced], + }); + } + + getSearchResult = (query = '') => { if (!this.searchResult[query]) { return undefined; } - return this.searchResult[query].map((id: Alert['pk']) => this.items[id]); - } + }; - async getAlertGroupsForIntegration(integrationId: AlertReceiveChannel['id']) { + async getAlertGroupsForIntegration(integrationId: ApiSchemas['AlertReceiveChannel']['id']) { const { results } = await makeRequest(`${this.path}`, { params: { integration: integrationId }, }); @@ -202,7 +232,7 @@ export class AlertGroupStore extends BaseStore { await this.fetchTableSettings(); } - @action + @action.bound async updateBulkActions() { const response = await makeRequest(`${this.path}bulk_action_options/`, {}); @@ -242,12 +272,12 @@ export class AlertGroupStore extends BaseStore { this.setLiveUpdatesPaused(false); } - @action + @action.bound setLiveUpdatesPaused(value: boolean) { this.liveUpdatesPaused = value; } - @action + @action.bound @AutoLoadingState(ActionKey.UPDATE_FILTERS_AND_FETCH_INCIDENTS) async updateIncidentFiltersAndRefetchIncidentsAndStats(params: any, keepCursor = false) { if (!keepCursor) { @@ -257,21 +287,21 @@ export class AlertGroupStore extends BaseStore { await this.fetchIncidentsAndStats(); } - @action + @action.bound async updateIncidentsCursor(cursor: string) { this.setIncidentsCursor(cursor); this.fetchAlertGroups(); } - @action + @action.bound async setIncidentsCursor(cursor: string) { this.incidentsCursor = cursor; LocationHelper.update({ cursor }, 'partial'); } - @action + @action.bound async setIncidentsItemsPerPage() { this.setIncidentsCursor(undefined); @@ -360,7 +390,7 @@ export class AlertGroupStore extends BaseStore { return await makeRequest(`/alerts/${pk}`, {}); } - @action + @action.bound async getNewIncidentsStats() { const result = await makeRequest(`${this.path}stats/`, { params: { @@ -374,7 +404,7 @@ export class AlertGroupStore extends BaseStore { }); } - @action + @action.bound async getAcknowledgedIncidentsStats() { const result = await makeRequest(`${this.path}stats/`, { params: { @@ -388,7 +418,7 @@ export class AlertGroupStore extends BaseStore { }); } - @action + @action.bound async getResolvedIncidentsStats() { const result = await makeRequest(`${this.path}stats/`, { params: { @@ -402,7 +432,7 @@ export class AlertGroupStore extends BaseStore { }); } - @action + @action.bound async getSilencedIncidentsStats() { const result = await makeRequest(`${this.path}stats/`, { params: { @@ -416,7 +446,7 @@ export class AlertGroupStore extends BaseStore { }); } - @action + @action.bound async doIncidentAction(alertId: Alert['pk'], action: AlertAction, isUndo = false, data?: any) { this.updateAlert(alertId, { loading: true }); @@ -463,7 +493,7 @@ export class AlertGroupStore extends BaseStore { } } - @action + @action.bound async updateAlert(pk: Alert['pk'], value: Partial) { this.alerts.set(pk, { ...(this.alerts.get(pk) as Alert), @@ -478,7 +508,7 @@ export class AlertGroupStore extends BaseStore { }).catch(this.onApiError); } - @action + @action.bound async fetchTableSettings(): Promise { const tableSettings = await makeRequest('/alertgroup_table_settings', {}); @@ -493,7 +523,7 @@ export class AlertGroupStore extends BaseStore { }); } - @action + @action.bound @AutoLoadingState(ActionKey.ADD_NEW_COLUMN_TO_ALERT_GROUP) async updateTableSettings( columns: { visible: AlertGroupColumn[]; hidden: AlertGroupColumn[] }, diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index 8a3b4122..13779433 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -1,8 +1,8 @@ -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { Channel } from 'models/channel/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'; +import { ApiSchemas } from 'network/oncall-api/api.types'; export enum IncidentStatus { 'Firing', @@ -79,7 +79,7 @@ export interface Alert { status: IncidentStatus; short?: boolean; root_alert_group?: Alert; - alert_receive_channel: Partial; + alert_receive_channel: Partial; paged_users: PagedUser[]; team: GrafanaTeam['id']; grafana_incident_id: string | null; diff --git a/grafana-plugin/src/models/api_token/api_token.ts b/grafana-plugin/src/models/api_token/api_token.ts index ed394493..ec12d10d 100644 --- a/grafana-plugin/src/models/api_token/api_token.ts +++ b/grafana-plugin/src/models/api_token/api_token.ts @@ -21,7 +21,7 @@ export class ApiTokenStore extends BaseStore { this.path = '/tokens/'; } - @action + @action.bound async updateItems(query = '') { const results = await makeRequest(`${this.path}`, { params: { search: query }, @@ -46,13 +46,13 @@ export class ApiTokenStore extends BaseStore { }); } - getSearchResult(query = '') { + getSearchResult = (query = '') => { if (!this.searchResult[query]) { return undefined; } return this.searchResult[query].map((apiTokenId: ApiToken['id']) => this.items[apiTokenId]); - } + }; async revokeApiToken(id: ApiToken['id']) { return await makeRequest(`${this.path}${id}/`, { diff --git a/grafana-plugin/src/models/base_store.ts b/grafana-plugin/src/models/base_store.ts index 5fd717f4..9d365a03 100644 --- a/grafana-plugin/src/models/base_store.ts +++ b/grafana-plugin/src/models/base_store.ts @@ -42,7 +42,7 @@ export class BaseStore { throw error; } - @action + @action.bound async getAll(query = '') { return await makeRequest(`${this.path}`, { params: { search: query }, @@ -50,7 +50,7 @@ export class BaseStore { }).catch(this.onApiError); } - @action + @action.bound async getById(id: string, skipErrorHandling = false, fromOrganization = false) { return await makeRequest(`${this.path}${id}`, { method: 'GET', @@ -58,7 +58,7 @@ export class BaseStore { }).catch((error) => this.onApiError(error, skipErrorHandling)); } - @action + @action.bound async create(data: any, skipErrorHandling = false): Promise { return await makeRequest(this.path, { method: 'POST', @@ -68,7 +68,7 @@ export class BaseStore { }); } - @action + @action.bound async update(id: any, data: any, params: any = null, skipErrorHandling = false): Promise { const result = await makeRequest(`${this.path}${id}/`, { method: 'PUT', @@ -83,7 +83,7 @@ export class BaseStore { return result; } - @action + @action.bound async delete(id: any) { const result = await makeRequest(`${this.path}${id}/`, { method: 'DELETE', diff --git a/grafana-plugin/src/models/channel_filter/channel_filter.types.ts b/grafana-plugin/src/models/channel_filter/channel_filter.types.ts index e842f7a0..ea8bf9f3 100644 --- a/grafana-plugin/src/models/channel_filter/channel_filter.types.ts +++ b/grafana-plugin/src/models/channel_filter/channel_filter.types.ts @@ -1,7 +1,7 @@ -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'; import { SlackChannel } from 'models/slack_channel/slack_channel.types'; import { TelegramChannel, TelegramChannelDetails } from 'models/telegram_channel/telegram_channel.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; export enum FilteringTermType { regex, @@ -10,7 +10,7 @@ export enum FilteringTermType { export interface ChannelFilter { id: string; - alert_receive_channel: AlertReceiveChannel['id']; + alert_receive_channel: ApiSchemas['AlertReceiveChannel']['id']; slack_channel_id?: SlackChannel['id']; slack_channel?: SlackChannel; telegram_channel?: TelegramChannel['id']; diff --git a/grafana-plugin/src/models/cloud/cloud.ts b/grafana-plugin/src/models/cloud/cloud.ts index 085c3443..6a20bc90 100644 --- a/grafana-plugin/src/models/cloud/cloud.ts +++ b/grafana-plugin/src/models/cloud/cloud.ts @@ -24,7 +24,7 @@ export class CloudStore extends BaseStore { this.path = '/cloud_users/'; } - @action + @action.bound async updateItems(page = 1) { const { matched_users_count, results } = await makeRequest(this.path, { params: { page }, @@ -49,12 +49,12 @@ export class CloudStore extends BaseStore { }); } - getSearchResult() { + getSearchResult = () => { return { matched_users_count: this.searchResult.matched_users_count, results: this.searchResult.results && this.searchResult.results.map((id: Cloud['id']) => this.items?.[id]), }; - } + }; async syncCloudUsers() { return await makeRequest(`${this.path}`, { method: 'POST' }); diff --git a/grafana-plugin/src/models/direct_paging/direct_paging.ts b/grafana-plugin/src/models/direct_paging/direct_paging.ts index 38a903b7..636211e5 100644 --- a/grafana-plugin/src/models/direct_paging/direct_paging.ts +++ b/grafana-plugin/src/models/direct_paging/direct_paging.ts @@ -29,7 +29,7 @@ export class DirectPagingStore extends BaseStore { this.path = '/direct_paging/'; } - @action + @action.bound addUserToSelectedUsers = (user: UserCurrentlyOnCall) => { this.selectedUserResponders = [ ...this.selectedUserResponders, @@ -40,22 +40,22 @@ export class DirectPagingStore extends BaseStore { ]; }; - @action + @action.bound resetSelectedUsers = () => { this.selectedUserResponders = []; }; - @action + @action.bound updateSelectedTeam = (team: GrafanaTeam) => { this.selectedTeamResponder = team; }; - @action + @action.bound resetSelectedTeam = () => { this.selectedTeamResponder = null; }; - @action + @action.bound removeSelectedUser(index: number) { this.selectedUserResponders = [ ...this.selectedUserResponders.slice(0, index), @@ -63,7 +63,7 @@ export class DirectPagingStore extends BaseStore { ]; } - @action + @action.bound updateSelectedUserImportantStatus(index: number, important: boolean) { this.selectedUserResponders = [ ...this.selectedUserResponders.slice(0, index), diff --git a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts index 5b55d8fa..0efd785d 100644 --- a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts +++ b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts @@ -30,7 +30,7 @@ export class EscalationChainStore extends BaseStore { this.path = '/escalation_chains/'; } - @action + @action.bound async loadItem(id: EscalationChain['id'], skipErrorHandling = false): Promise { const escalationChain = await this.getById(id, skipErrorHandling); @@ -44,7 +44,7 @@ export class EscalationChainStore extends BaseStore { return escalationChain; } - @action + @action.bound async updateById(id: EscalationChain['id']) { const response = await this.getById(id); @@ -56,7 +56,7 @@ export class EscalationChainStore extends BaseStore { }); } - @action + @action.bound async save(id: EscalationChain['id'], data: Partial) { const response = await super.update(id, data); @@ -68,7 +68,7 @@ export class EscalationChainStore extends BaseStore { }); } - @action + @action.bound async updateEscalationChainDetails(id: EscalationChain['id']) { const response = await makeRequest(`${this.path}${id}/details/`, {}); @@ -80,7 +80,7 @@ export class EscalationChainStore extends BaseStore { }); } - @action + @action.bound async updateItem(id: EscalationChain['id'], skipErrorHandling = false): Promise { let escalationChain; try { @@ -107,7 +107,7 @@ export class EscalationChainStore extends BaseStore { return escalationChain; } - @action + @action.bound async updateItems(query: any = '') { const params = typeof query === 'string' ? { search: query } : query; @@ -140,13 +140,13 @@ export class EscalationChainStore extends BaseStore { this.loading = false; } - getSearchResult(query = '') { + getSearchResult = (query = '') => { if (!this.searchResult[query]) { return undefined; } return this.searchResult[query].map((escalationChainId: EscalationChain['id']) => this.items[escalationChainId]); - } + }; clone = (escalationChainId: EscalationChain['id'], data: Partial): Promise => makeRequest(`${this.path}${escalationChainId}/copy/`, { diff --git a/grafana-plugin/src/models/escalation_policy/escalation_policy.ts b/grafana-plugin/src/models/escalation_policy/escalation_policy.ts index f557d294..81b394ee 100644 --- a/grafana-plugin/src/models/escalation_policy/escalation_policy.ts +++ b/grafana-plugin/src/models/escalation_policy/escalation_policy.ts @@ -64,7 +64,7 @@ export class EscalationPolicyStore extends BaseStore { }); } - @action + @action.bound async updateEscalationPolicies(escalationChainId: EscalationChain['id']) { const response = await makeRequest(this.path, { params: { escalation_chain: escalationChainId }, @@ -91,7 +91,7 @@ export class EscalationPolicyStore extends BaseStore { }); } - @action + @action.bound createEscalationPolicy(escalationChainId: EscalationChain['id'], data: Partial) { return super.create({ ...data, @@ -99,7 +99,7 @@ export class EscalationPolicyStore extends BaseStore { }); } - @action + @action.bound async saveEscalationPolicy(id: EscalationPolicy['id'], data: Partial) { this.items[id] = { ...this.items[id], @@ -113,7 +113,7 @@ export class EscalationPolicyStore extends BaseStore { } } - @action + @action.bound async moveEscalationPolicyToPosition(oldIndex: any, newIndex: any, escalationChainId: EscalationChain['id']) { const escalationPolicyId = this.escalationChainToEscalationPolicy[escalationChainId][oldIndex]; @@ -130,7 +130,7 @@ export class EscalationPolicyStore extends BaseStore { this.updateEscalationPolicies(escalationChainId); } - @action + @action.bound async deleteEscalationPolicy(data: Partial) { const index = this.escalationChainToEscalationPolicy[data.escalation_chain].findIndex( (escalationPolicyId: EscalationPolicy['id']) => escalationPolicyId === data.id diff --git a/grafana-plugin/src/models/filters/filters.ts b/grafana-plugin/src/models/filters/filters.ts index cb2d32b0..d97c5486 100644 --- a/grafana-plugin/src/models/filters/filters.ts +++ b/grafana-plugin/src/models/filters/filters.ts @@ -39,7 +39,7 @@ export class FiltersStore extends BaseStore { } } - @action + @action.bound setNeedToParseFilters(value: boolean) { this.needToParseFilters = value; } @@ -54,7 +54,7 @@ export class FiltersStore extends BaseStore { return this._globalValues; } - @action + @action.bound public async updateOptionsForPage(page: string) { const result = await makeRequest(`/${getApiPathByPage(page)}/filters/`, {}); @@ -73,7 +73,7 @@ export class FiltersStore extends BaseStore { return result; } - @action + @action.bound updateValuesForPage(page: string, value: FiltersValues) { this.values = { ...this.values, @@ -81,12 +81,12 @@ export class FiltersStore extends BaseStore { }; } - @action + @action.bound setCurrentTablePageNum(page: PAGE, currentTablePageNum: number) { this.currentTablePageNum[page] = currentTablePageNum; } - @action + @action.bound applyLabelFilter = (label: LabelKeyValue, page: PAGE) => { const currentLabelFilterValues = this.values[page]?.label || []; const labelToAddString = `${label.key.id}:${label.value.id}`; diff --git a/grafana-plugin/src/models/global_setting/global_setting.ts b/grafana-plugin/src/models/global_setting/global_setting.ts index 27fe804c..54612d11 100644 --- a/grafana-plugin/src/models/global_setting/global_setting.ts +++ b/grafana-plugin/src/models/global_setting/global_setting.ts @@ -20,7 +20,7 @@ export class GlobalSettingStore extends BaseStore { this.path = '/live_settings/'; } - @action + @action.bound async updateById(id: GlobalSetting['id']) { const response = await this.getById(id); @@ -32,7 +32,7 @@ export class GlobalSettingStore extends BaseStore { }); } - @action + @action.bound async updateItems(query = '') { const results = await this.getAll(); @@ -55,13 +55,13 @@ export class GlobalSettingStore extends BaseStore { }); } - getSearchResult(query = '') { + getSearchResult = (query = '') => { if (!this.searchResult[query]) { return undefined; } return this.searchResult[query].map((globalSettingId: GlobalSetting['id']) => this.items[globalSettingId]); - } + }; async getGlobalSettingItemByName(name: string) { const results = await this.getAll(); diff --git a/grafana-plugin/src/models/grafana_team/grafana_team.ts b/grafana-plugin/src/models/grafana_team/grafana_team.ts index 01323e98..799e5242 100644 --- a/grafana-plugin/src/models/grafana_team/grafana_team.ts +++ b/grafana-plugin/src/models/grafana_team/grafana_team.ts @@ -22,7 +22,7 @@ export class GrafanaTeamStore extends BaseStore { this.path = '/teams/'; } - @action + @action.bound async updateTeam(id: GrafanaTeam['id'], data: Partial) { const result = await this.update(id, data); @@ -61,7 +61,7 @@ export class GrafanaTeamStore extends BaseStore { }); } - getSearchResult() { + getSearchResult = () => { return this.searchResult.map((teamId: GrafanaTeam['id']) => this.items[teamId]); - } + }; } diff --git a/grafana-plugin/src/models/heartbeat/heartbeat.ts b/grafana-plugin/src/models/heartbeat/heartbeat.ts index 7ee67a2b..5896dd11 100644 --- a/grafana-plugin/src/models/heartbeat/heartbeat.ts +++ b/grafana-plugin/src/models/heartbeat/heartbeat.ts @@ -1,8 +1,8 @@ import { action, observable, makeObservable, runInAction } from 'mobx'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { BaseStore } from 'models/base_store'; import { makeRequest } from 'network/network'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { RootStore } from 'state/rootStore'; import { Heartbeat } from './heartbeat.types'; @@ -22,7 +22,7 @@ export class HeartbeatStore extends BaseStore { this.path = '/heartbeats/'; } - @action + @action.bound async updateTimeoutOptions() { const result = await makeRequest(`${this.path}timeout_options/`, {}); @@ -31,7 +31,7 @@ export class HeartbeatStore extends BaseStore { }); } - @action + @action.bound async saveHeartbeat(id: Heartbeat['id'], data: Partial) { const response = await super.update(id, data); @@ -47,8 +47,8 @@ export class HeartbeatStore extends BaseStore { }); } - @action - async createHeartbeat(alertReceiveChannelId: AlertReceiveChannel['id'], data: Partial) { + @action.bound + async createHeartbeat(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], data: Partial) { const response = await super.create({ alert_receive_channel: alertReceiveChannelId, ...data, diff --git a/grafana-plugin/src/models/heartbeat/heartbeat.types.ts b/grafana-plugin/src/models/heartbeat/heartbeat.types.ts index b746a7e3..22aff47d 100644 --- a/grafana-plugin/src/models/heartbeat/heartbeat.types.ts +++ b/grafana-plugin/src/models/heartbeat/heartbeat.types.ts @@ -1,9 +1,9 @@ -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; export interface Heartbeat { id: string; last_heartbeat_time_verbal: string; - alert_receive_channel: AlertReceiveChannel['id']; + alert_receive_channel: ApiSchemas['AlertReceiveChannel']['id']; link: string; timeout_seconds: number; status: boolean; diff --git a/grafana-plugin/src/models/label/label.ts b/grafana-plugin/src/models/label/label.ts index 199d7740..fabef1de 100644 --- a/grafana-plugin/src/models/label/label.ts +++ b/grafana-plugin/src/models/label/label.ts @@ -3,7 +3,7 @@ import { action, makeObservable } from 'mobx'; import { BaseStore } from 'models/base_store'; import { makeRequest } from 'network/network'; import { ApiSchemas } from 'network/oncall-api/api.types'; -import onCallApi from 'network/oncall-api/http-client'; +import { onCallApi } from 'network/oncall-api/http-client'; import { RootStore } from 'state/rootStore'; import { WithGlobalNotification } from 'utils/decorators'; @@ -18,7 +18,7 @@ export class LabelStore extends BaseStore { @action.bound public async loadKeys(search = '') { - const { data } = await onCallApi.GET('/labels/keys/', undefined); + const { data } = await onCallApi().GET('/labels/keys/', undefined); const filtered = data.filter((k) => k.name.toLowerCase().includes(search.toLowerCase())); diff --git a/grafana-plugin/src/models/loader/loader.ts b/grafana-plugin/src/models/loader/loader.ts index a24822d9..846fe4d2 100644 --- a/grafana-plugin/src/models/loader/loader.ts +++ b/grafana-plugin/src/models/loader/loader.ts @@ -22,6 +22,10 @@ class LoaderStoreClass { this.items[actionKey] = isLoading; } } + + isLoading(actionKey: string): boolean { + return !!this.items[actionKey]; + } } export const LoaderStore = new LoaderStoreClass(); diff --git a/grafana-plugin/src/models/msteams_channel/msteams_channel.ts b/grafana-plugin/src/models/msteams_channel/msteams_channel.ts index ecf2cc8a..bc36529b 100644 --- a/grafana-plugin/src/models/msteams_channel/msteams_channel.ts +++ b/grafana-plugin/src/models/msteams_channel/msteams_channel.ts @@ -26,7 +26,7 @@ export class MSTeamsChannelStore extends BaseStore { this.path = '/msteams/channels/'; } - @action + @action.bound async updateMSTeamsChannels() { const response = await makeRequest(this.path, {}); @@ -48,7 +48,7 @@ export class MSTeamsChannelStore extends BaseStore { }); } - @action + @action.bound async updateById(id: MSTeamsChannel['id']) { const response = await this.getById(id); @@ -60,7 +60,7 @@ export class MSTeamsChannelStore extends BaseStore { }); } - @action + @action.bound async updateItems(query = '') { const result = await this.getAll(); @@ -83,12 +83,12 @@ export class MSTeamsChannelStore extends BaseStore { }); } - getSearchResult(query = '') { + getSearchResult = (query = '') => { if (!this.searchResult[query]) { return undefined; } return this.searchResult[query].map((msteamsChannelId: MSTeamsChannel['id']) => this.items[msteamsChannelId]); - } + }; @computed get hasItems() { diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts index 6125f24e..9728453d 100644 --- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts +++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts @@ -28,7 +28,7 @@ export class OutgoingWebhookStore extends BaseStore { this.path = '/webhooks/'; } - @action + @action.bound async loadItem(id: OutgoingWebhook['id'], skipErrorHandling = false): Promise { const outgoingWebhook = await this.getById(id, skipErrorHandling); @@ -42,7 +42,7 @@ export class OutgoingWebhookStore extends BaseStore { return outgoingWebhook; } - @action + @action.bound async updateById(id: OutgoingWebhook['id']) { const response = await this.getById(id); @@ -54,7 +54,7 @@ export class OutgoingWebhookStore extends BaseStore { }); } - @action + @action.bound async updateItem(id: OutgoingWebhook['id'], fromOrganization = false) { const response = await this.getById(id, false, fromOrganization); @@ -66,7 +66,7 @@ export class OutgoingWebhookStore extends BaseStore { }); } - @action + @action.bound async updateItems(query: any = '') { const params = typeof query === 'string' ? { search: query } : query; @@ -95,13 +95,13 @@ export class OutgoingWebhookStore extends BaseStore { }); } - getSearchResult(query = '') { + getSearchResult = (query = '') => { if (!this.searchResult[query]) { return undefined; } return this.searchResult[query].map((outgoingWebhookId: OutgoingWebhook['id']) => this.items[outgoingWebhookId]); - } + }; async getLastResponses(id: OutgoingWebhook['id']) { const result = await makeRequest(`${this.path}${id}/responses`, {}); diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index 72f12e41..c3b5239b 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -133,7 +133,7 @@ export class ScheduleStore extends BaseStore { this.path = '/schedules/'; } - @action + @action.bound async loadItem(id: Schedule['id'], skipErrorHandling = false): Promise { const schedule = await this.getById(id, skipErrorHandling); @@ -147,7 +147,7 @@ export class ScheduleStore extends BaseStore { return schedule; } - @action + @action.bound async updateItems( f: RemoteFiltersType | string = { searchTerm: '', type: undefined, used: undefined }, page = 1, @@ -182,7 +182,7 @@ export class ScheduleStore extends BaseStore { }); } - @action + @action.bound async updateItem(id: Schedule['id'], fromOrganization = false) { if (id) { let schedule; @@ -211,13 +211,13 @@ export class ScheduleStore extends BaseStore { } } - getSearchResult() { + getSearchResult = () => { return { page_size: this.searchResult.page_size, count: this.searchResult.count, results: this.searchResult.results?.map((scheduleId: Schedule['id']) => this.items[scheduleId]), }; - } + }; @action.bound async getScoreQuality(scheduleId: Schedule['id']) { @@ -256,7 +256,7 @@ export class ScheduleStore extends BaseStore { // ------- NEW SCHEDULES API ENDPOINTS --------- - @action + @action.bound async createRotation(scheduleId: Schedule['id'], isOverride: boolean, params: Partial) { const type = isOverride ? 3 : 2; @@ -324,7 +324,7 @@ export class ScheduleStore extends BaseStore { }); } - @action + @action.bound async updateShiftsSwapPreview(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, params: Partial) { const fromString = getFromString(startMoment); @@ -350,7 +350,7 @@ export class ScheduleStore extends BaseStore { }); } - @action + @action.bound clearPreview() { this.finalPreview = undefined; this.rotationPreview = undefined; @@ -359,7 +359,7 @@ export class ScheduleStore extends BaseStore { this.rotationFormLiveParams = undefined; } - @action + @action.bound async updateRotation(shiftId: Shift['id'], params: Partial) { const response = await makeRequest(`/oncall_shifts/${shiftId}`, { params: { force: true }, @@ -377,7 +377,7 @@ export class ScheduleStore extends BaseStore { return response; } - @action + @action.bound async updateRotationAsNew(shiftId: Shift['id'], params: Partial) { const response = await makeRequest(`/oncall_shifts/${shiftId}`, { data: { ...params }, @@ -394,7 +394,7 @@ export class ScheduleStore extends BaseStore { return response; } - @action + @action.bound updateRelatedEscalationChains = async (id: Schedule['id']) => { const response = await makeRequest(`/schedules/${id}/related_escalation_chains`, { method: 'GET', @@ -410,7 +410,7 @@ export class ScheduleStore extends BaseStore { return response; }; - @action + @action.bound updateRelatedUsers = async (id: Schedule['id']) => { const { users } = await makeRequest(`/schedules/${id}/next_shifts_per_user`, { method: 'GET', @@ -426,7 +426,7 @@ export class ScheduleStore extends BaseStore { return users; }; - @action + @action.bound async updateOncallShifts(scheduleId: Schedule['id']) { const { results } = await makeRequest(`/oncall_shifts/`, { params: { @@ -449,7 +449,7 @@ export class ScheduleStore extends BaseStore { }); } - @action + @action.bound async updateOncallShift(shiftId: Shift['id']) { if (this.shiftsCurrentlyUpdating[shiftId]) { return; @@ -471,7 +471,7 @@ export class ScheduleStore extends BaseStore { return response; } - @action + @action.bound async saveOncallShift(shiftId: Shift['id'], data: Partial) { const response = await makeRequest(`/oncall_shifts/${shiftId}`, { method: 'PUT', data }); @@ -492,7 +492,7 @@ export class ScheduleStore extends BaseStore { }).catch(this.onApiError); } - @action + @action.bound async updateEvents(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, type: RotationType = 'rotation', days = 9) { const dayBefore = startMoment.subtract(1, 'day'); @@ -554,7 +554,7 @@ export class ScheduleStore extends BaseStore { }); } - @action + @action.bound async updateDaysOptions() { const result = await makeRequest(`/oncall_shifts/days_options/`, { method: 'GET', @@ -577,7 +577,7 @@ export class ScheduleStore extends BaseStore { return await makeRequest(`/shift_swaps/${shiftSwapId}/take`, { method: 'POST' }).catch(this.onApiError); } - @action + @action.bound async loadShiftSwap(id: ShiftSwap['id']) { const result = await makeRequest(`/shift_swaps/${id}`, { params: { expand_users: true } }); @@ -588,7 +588,7 @@ export class ScheduleStore extends BaseStore { return result; } - @action + @action.bound async updateShiftSwaps(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, days = 9) { const fromString = getFromString(startMoment); @@ -630,7 +630,7 @@ export class ScheduleStore extends BaseStore { } @AutoLoadingState(ActionKey.UPDATE_PERSONAL_EVENTS) - @action + @action.bound async updatePersonalEvents(userPk: User['pk'], startMoment: dayjs.Dayjs, days = 9, isUpdateOnCallNow = false) { const fromString = getFromString(startMoment); diff --git a/grafana-plugin/src/models/slack/slack.ts b/grafana-plugin/src/models/slack/slack.ts index 982b37fe..c57838c2 100644 --- a/grafana-plugin/src/models/slack/slack.ts +++ b/grafana-plugin/src/models/slack/slack.ts @@ -19,7 +19,7 @@ export class SlackStore extends BaseStore { makeObservable(this); } - @action + @action.bound async updateSlackSettings() { const result = await makeRequest('/slack_settings/', {}); @@ -28,7 +28,7 @@ export class SlackStore extends BaseStore { }); } - @action + @action.bound async saveSlackSettings(data: Partial) { const result = await makeRequest('/slack_settings/', { data, @@ -40,7 +40,7 @@ export class SlackStore extends BaseStore { }); } - @action + @action.bound async setGeneralLogChannelId(id: SlackChannel['id']) { return await makeRequest('/set_general_channel/', { method: 'POST', @@ -48,7 +48,7 @@ export class SlackStore extends BaseStore { }); } - @action + @action.bound async updateSlackIntegrationData(slack_id: string) { const result = await makeRequest('/slack_integration/', { params: { slack_id }, diff --git a/grafana-plugin/src/models/slack_channel/slack_channel.ts b/grafana-plugin/src/models/slack_channel/slack_channel.ts index 3b407911..4053129d 100644 --- a/grafana-plugin/src/models/slack_channel/slack_channel.ts +++ b/grafana-plugin/src/models/slack_channel/slack_channel.ts @@ -21,7 +21,7 @@ export class SlackChannelStore extends BaseStore { this.path = '/slack_channels/'; } - @action // deprecated, use updateItem instead + @action.bound // deprecated, use updateItem instead async updateById(id: SlackChannel['id']) { const response = await this.getById(id); @@ -33,7 +33,7 @@ export class SlackChannelStore extends BaseStore { }); } - @action + @action.bound async updateItem(id: SlackChannel['id']) { const response = await this.getById(id); @@ -45,7 +45,7 @@ export class SlackChannelStore extends BaseStore { }); } - @action + @action.bound async updateItems(query = '') { const { results } = await makeRequest(`${this.path}`, { params: { search: query }, @@ -70,11 +70,11 @@ export class SlackChannelStore extends BaseStore { }); } - getSearchResult(query = '') { + getSearchResult = (query = '') => { if (!this.searchResult[query]) { return undefined; } return this.searchResult[query].map((slackChannelId: SlackChannel['id']) => this.items[slackChannelId]); - } + }; } diff --git a/grafana-plugin/src/models/telegram_channel/telegram_channel.ts b/grafana-plugin/src/models/telegram_channel/telegram_channel.ts index 112eab4b..d462181e 100644 --- a/grafana-plugin/src/models/telegram_channel/telegram_channel.ts +++ b/grafana-plugin/src/models/telegram_channel/telegram_channel.ts @@ -26,7 +26,7 @@ export class TelegramChannelStore extends BaseStore { this.path = '/telegram_channels/'; } - @action + @action.bound async updateTelegramChannels() { const response = await makeRequest(this.path, {}); @@ -48,7 +48,7 @@ export class TelegramChannelStore extends BaseStore { }); } - @action + @action.bound async updateById(id: TelegramChannel['id']) { const response = await this.getById(id); @@ -60,7 +60,7 @@ export class TelegramChannelStore extends BaseStore { }); } - @action + @action.bound async updateItems(query = '') { const result = await this.getAll(); @@ -83,12 +83,12 @@ export class TelegramChannelStore extends BaseStore { }); } - getSearchResult(query = '') { + getSearchResult = (query = '') => { if (!this.searchResult[query]) { return undefined; } return this.searchResult[query].map((telegramChannelId: TelegramChannel['id']) => this.items[telegramChannelId]); - } + }; @computed get hasItems() { @@ -111,14 +111,14 @@ export class TelegramChannelStore extends BaseStore { }); } - @action + @action.bound async makeTelegramChannelDefault(id: TelegramChannel['id']) { return makeRequest(`/telegram_channels/${id}/set_default/`, { method: 'POST', }); } - @action + @action.bound async deleteTelegramChannel(id: TelegramChannel['id']) { return super.delete(id); } diff --git a/grafana-plugin/src/models/timezone/timezone.helpers.ts b/grafana-plugin/src/models/timezone/timezone.helpers.ts index 1396ae1f..18873843 100644 --- a/grafana-plugin/src/models/timezone/timezone.helpers.ts +++ b/grafana-plugin/src/models/timezone/timezone.helpers.ts @@ -595,8 +595,6 @@ export const allTimezones = [ 'Zulu', ]; -// TODO: move it to utils - export const getTzOffsetString = (date: dayjs.Dayjs) => { const userOffset = date.utcOffset(); const userOffsetHours = userOffset / 60; diff --git a/grafana-plugin/src/models/user/user.test.ts b/grafana-plugin/src/models/user/user.test.ts index afcc1238..b385a910 100644 --- a/grafana-plugin/src/models/user/user.test.ts +++ b/grafana-plugin/src/models/user/user.test.ts @@ -41,7 +41,7 @@ describe('UserStore.unlinkBackend', () => { test('it makes the proper API call and returns the response', async () => { makeRequest.mockResolvedValueOnce('hello'); - userStore.loadCurrentUser = jest.fn(); + Object.defineProperty(userStore, 'loadCurrentUser', { value: jest.fn() }); await userStore.unlinkBackend(userPk, backend); diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index f21010fd..94985418 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -60,7 +60,7 @@ export class UserStore extends BaseStore { return this.items[this.currentUserPk as User['pk']]; } - @action + @action.bound async loadCurrentUser() { const response = await makeRequest('/user/', {}); const timezone = await this.refreshTimezone(response.pk); @@ -74,7 +74,7 @@ export class UserStore extends BaseStore { }); } - @action + @action.bound async refreshTimezone(id: User['pk']) { const { timezone: grafanaPreferencesTimezone } = config.bootData.user; const timezone = grafanaPreferencesTimezone === 'browser' ? dayjs.tz.guess() : grafanaPreferencesTimezone; @@ -87,7 +87,7 @@ export class UserStore extends BaseStore { return timezone; } - @action + @action.bound async loadUser(userPk: User['pk'], skipErrorHandling = false): Promise { const user = await this.getById(userPk, skipErrorHandling); @@ -101,7 +101,7 @@ export class UserStore extends BaseStore { return user; } - @action + @action.bound async updateItem(userPk: User['pk']) { if (this.itemsCurrentlyUpdating[userPk]) { return; @@ -132,7 +132,7 @@ export class UserStore extends BaseStore { }); } - @action + @action.bound async updateItems(f: any = { searchTerm: '' }, page = 1, invalidateFn?: () => boolean): Promise { const response = await this.search(f, page); @@ -167,19 +167,19 @@ export class UserStore extends BaseStore { return response; } - getSearchResult() { + getSearchResult = () => { return { page_size: this.searchResult.page_size, count: this.searchResult.count, results: this.searchResult.results?.map((userPk: User['pk']) => this.items?.[userPk]), }; - } + }; sendTelegramConfirmationCode = async (userPk: User['pk']) => { return await makeRequest(`/users/${userPk}/get_telegram_verification_code/`, {}); }; - @action + @action.bound unlinkSlack = async (userPk: User['pk']) => { await makeRequest(`/users/${userPk}/unlink_slack/`, { method: 'POST', @@ -195,7 +195,7 @@ export class UserStore extends BaseStore { }); }; - @action + @action.bound unlinkTelegram = async (userPk: User['pk']) => { await makeRequest(`/users/${userPk}/unlink_telegram/`, { method: 'POST', @@ -216,7 +216,7 @@ export class UserStore extends BaseStore { method: 'GET', }); - @action + @action.bound unlinkBackend = async (userPk: User['pk'], backend: string) => { await makeRequest(`/users/${userPk}/unlink_backend/?backend=${backend}`, { method: 'POST', @@ -225,7 +225,7 @@ export class UserStore extends BaseStore { this.loadCurrentUser(); }; - @action + @action.bound async createUser(data: any) { const user = await this.create(data); @@ -239,7 +239,7 @@ export class UserStore extends BaseStore { return user; } - @action + @action.bound async updateUser(data: Partial) { const user = await makeRequest(`/users/${data.pk}/`, { method: 'PUT', @@ -261,7 +261,7 @@ export class UserStore extends BaseStore { }); } - @action + @action.bound async updateCurrentUser(data: Partial) { const user = await makeRequest(`/user/`, { method: 'PUT', @@ -279,7 +279,7 @@ export class UserStore extends BaseStore { }); } - @action + @action.bound async fetchVerificationCode(userPk: User['pk'], recaptchaToken: string) { await makeRequest(`/users/${userPk}/get_verification_code/`, { method: 'GET', @@ -287,7 +287,7 @@ export class UserStore extends BaseStore { }).catch(throttlingError); } - @action + @action.bound async fetchVerificationCall(userPk: User['pk'], recaptchaToken: string) { await makeRequest(`/users/${userPk}/get_verification_call/`, { method: 'GET', @@ -295,21 +295,21 @@ export class UserStore extends BaseStore { }).catch(throttlingError); } - @action + @action.bound async verifyPhone(userPk: User['pk'], token: string) { return await makeRequest(`/users/${userPk}/verify_number/?token=${token}`, { method: 'PUT', }).catch(throttlingError); } - @action + @action.bound async forgetPhone(userPk: User['pk']) { return await makeRequest(`/users/${userPk}/forget_number/`, { method: 'PUT', }); } - @action + @action.bound async updateNotificationPolicies(id: User['pk']) { const importantEPs = await makeRequest('/notification_policies/', { params: { user: id, important: true }, @@ -327,7 +327,7 @@ export class UserStore extends BaseStore { }); } - @action + @action.bound async moveNotificationPolicyToPosition(userPk: User['pk'], oldIndex: number, newIndex: number, offset: number) { const notificationPolicy = this.notificationPolicies[userPk][oldIndex + offset]; @@ -342,7 +342,7 @@ export class UserStore extends BaseStore { this.updateItem(userPk); // to update notification_chain_verbal } - @action + @action.bound async addNotificationPolicy(userPk: User['pk'], important: NotificationPolicyType['important']) { await makeRequest(`/notification_policies/`, { method: 'POST', @@ -354,7 +354,7 @@ export class UserStore extends BaseStore { this.updateItem(userPk); // to update notification_chain_verbal } - @action + @action.bound async updateNotificationPolicy(userPk: User['pk'], id: NotificationPolicyType['id'], value: NotificationPolicyType) { this.notificationPolicies = { ...this.notificationPolicies, @@ -380,7 +380,7 @@ export class UserStore extends BaseStore { this.updateItem(userPk); // to update notification_chain_verbal } - @action + @action.bound async deleteNotificationPolicy(userPk: User['pk'], id: NotificationPolicyType['id']) { await makeRequest(`/notification_policies/${id}`, { method: 'DELETE' }).catch(this.onApiError); @@ -400,7 +400,7 @@ export class UserStore extends BaseStore { }); } - @action + @action.bound async sendTestPushNotification(userId: User['pk'], isCritical: boolean) { return await makeRequest(`/users/${userId}/send_test_push`, { method: 'POST', @@ -419,7 +419,7 @@ export class UserStore extends BaseStore { }); } - @action + @action.bound async makeTestCall(userPk: User['pk']) { this.isTestCallInProgress = true; diff --git a/grafana-plugin/src/models/user_group/user_group.ts b/grafana-plugin/src/models/user_group/user_group.ts index 4aa18697..5b70ea69 100644 --- a/grafana-plugin/src/models/user_group/user_group.ts +++ b/grafana-plugin/src/models/user_group/user_group.ts @@ -21,7 +21,7 @@ export class UserGroupStore extends BaseStore { this.path = '/user_groups/'; } - @action + @action.bound async updateItems(query = '') { const result = await makeRequest(`${this.path}`, { params: { search: query }, @@ -46,11 +46,11 @@ export class UserGroupStore extends BaseStore { }); } - getSearchResult(query = '') { + getSearchResult = (query = '') => { if (!this.searchResult[query]) { return undefined; } return this.searchResult[query].map((userGroupId: UserGroup['id']) => this.items?.[userGroupId]); - } + }; } diff --git a/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts b/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts index 2559c9be..5132dba7 100644 --- a/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts +++ b/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts @@ -1,6 +1,317 @@ import type { CustomApiSchemas } from './types-generator/custom-schemas'; export interface paths { + '/alert_receive_channels/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for alert receive channels (integrations). */ + get: operations['alert_receive_channels_list']; + put?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + post: operations['alert_receive_channels_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for alert receive channels (integrations). */ + get: operations['alert_receive_channels_retrieve']; + /** @description Internal API endpoints for alert receive channels (integrations). */ + put: operations['alert_receive_channels_update']; + post?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + delete: operations['alert_receive_channels_destroy']; + options?: never; + head?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + patch: operations['alert_receive_channels_partial_update']; + trace?: never; + }; + '/alert_receive_channels/{id}/change_team/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + put: operations['alert_receive_channels_change_team_update']; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/connect_contact_point/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + post: operations['alert_receive_channels_connect_contact_point_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/connected_contact_points/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for alert receive channels (integrations). */ + get: operations['alert_receive_channels_connected_contact_points_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/counters/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for alert receive channels (integrations). */ + get: operations['alert_receive_channels_counters_per_integration_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/create_contact_point/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + post: operations['alert_receive_channels_create_contact_point_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/disconnect_contact_point/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + post: operations['alert_receive_channels_disconnect_contact_point_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/migrate/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + post: operations['alert_receive_channels_migrate_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/preview_template/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Preview template */ + post: operations['alert_receive_channels_preview_template_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/send_demo_alert/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + post: operations['alert_receive_channels_send_demo_alert_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/start_maintenance/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + post: operations['alert_receive_channels_start_maintenance_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/{id}/stop_maintenance/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for alert receive channels (integrations). */ + post: operations['alert_receive_channels_stop_maintenance_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/contact_points/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for alert receive channels (integrations). */ + get: operations['alert_receive_channels_contact_points_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/counters/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for alert receive channels (integrations). */ + get: operations['alert_receive_channels_counters_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/filters/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for alert receive channels (integrations). */ + get: operations['alert_receive_channels_filters_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/integration_options/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for alert receive channels (integrations). */ + get: operations['alert_receive_channels_integration_options_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alert_receive_channels/validate_name/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Checks if verbal_name is available. + * It is needed for OnCall <-> Alerting integration. */ + get: operations['alert_receive_channels_validate_name_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/alertgroups/': { parameters: { query?: never; @@ -8,7 +319,7 @@ export interface paths { path?: never; cookie?: never; }; - /** @description Fetch a list of alert groups */ + /** @description Internal API endpoints for alert groups. */ get: operations['alertgroups_list']; put?: never; post?: never; @@ -25,11 +336,65 @@ export interface paths { path?: never; cookie?: never; }; - /** @description Fetch a single alert group */ + /** @description Return alert group details. + * + * It is worth mentioning that `render_after_resolve_report_json` property will return a list + * of log entries including actions involving the alert group, notifications triggered for a user + * and resolution notes updates. + * + * A few additional notes about the possible values for each key in the logs: + * + * - `time`: humanized time delta respect to now when the action took place + * - `action`: human-readable description of the action + * - `realm`: resource involved in the action; one of three possible values: + * `alert_group`, `user_notification`, `resolution_note` + * - `type`: integer value indicating the type of action (see below) + * - `created_at`: timestamp corresponding to when the action happened + * - `author`: details about the user performing the action + * + * Possible `type` values depending on the realm value: + * + * For `alert_group`: + * - 0: Acknowledged + * - 1: Unacknowledged + * - 2: Invite + * - 3: Stop invitation + * - 4: Re-invite + * - 5: Escalation triggered + * - 6: Invitation triggered + * - 7: Silenced + * - 8: Attached + * - 9: Unattached + * - 10: Custom button triggered + * - 11: Unacknowledged by timeout + * - 12: Failed attachment + * - 13: Incident resolved + * - 14: Incident unresolved + * - 15: Unsilenced + * - 16: Escalation finished + * - 17: Escalation failed + * - 18: Acknowledge reminder triggered + * - 19: Wiped + * - 20: Deleted + * - 21: Incident registered + * - 22: A route is assigned to the incident + * - 23: Trigger direct paging escalation + * - 24: Unpage a user + * - 25: Restricted + * + * For `user_notification`: + * - 0: Personal notification triggered + * - 1: Personal notification finished + * - 2: Personal notification success, + * - 3: Personal notification failed + * + * For `resolution_note`: + * - 0: slack + * - 1: web */ get: operations['alertgroups_retrieve']; put?: never; post?: never; - /** @description Delete an alert group */ + /** @description Internal API endpoints for alert groups. */ delete: operations['alertgroups_destroy']; options?: never; head?: never; @@ -70,6 +435,23 @@ export interface paths { patch?: never; trace?: never; }; + '/alertgroups/{id}/escalation_snapshot/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for alert groups. */ + get: operations['alertgroups_escalation_snapshot_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/alertgroups/{id}/preview_template/': { parameters: { query?: never; @@ -79,7 +461,7 @@ export interface paths { }; get?: never; put?: never; - /** @description Preview a template for an alert group */ + /** @description Preview template */ post: operations['alertgroups_preview_template_create']; delete?: never; options?: never; @@ -231,7 +613,7 @@ export interface paths { cookie?: never; }; /** @description Retrieve a list of valid bulk action options */ - get: operations['alertgroups_bulk_action_options_retrieve']; + get: operations['alertgroups_bulk_action_options_list']; put?: never; post?: never; delete?: never; @@ -248,7 +630,7 @@ export interface paths { cookie?: never; }; /** @description Retrieve a list of valid filter options that can be used to filter alert groups */ - get: operations['alertgroups_filters_retrieve']; + get: operations['alertgroups_filters_list']; put?: never; post?: never; delete?: never; @@ -300,7 +682,7 @@ export interface paths { cookie?: never; }; /** @description Retrieve a list of valid silence options */ - get: operations['alertgroups_silence_options_list']; + get: operations['alertgroups_silence_options_retrieve']; put?: never; post?: never; delete?: never; @@ -368,7 +750,7 @@ export interface paths { path?: never; cookie?: never; }; - /** @description Key with the list of values */ + /** @description get_key returns LabelOption – key with the list of values */ get: operations['labels_id_retrieve']; /** @description Rename the key */ put: operations['labels_id_update']; @@ -403,7 +785,7 @@ export interface paths { path?: never; cookie?: never; }; - /** @description Value name */ + /** @description get_value returns a Value */ get: operations['labels_id_values_retrieve']; /** @description Rename the value */ put: operations['labels_id_values_update']; @@ -431,15 +813,321 @@ export interface paths { patch?: never; trace?: never; }; + '/users/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for users. */ + get: operations['users_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for users. */ + get: operations['users_retrieve']; + /** @description Internal API endpoints for users. */ + put: operations['users_update']; + post?: never; + delete?: never; + options?: never; + head?: never; + /** @description Internal API endpoints for users. */ + patch: operations['users_partial_update']; + trace?: never; + }; + '/users/{id}/export_token/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for users. */ + get: operations['users_export_token_retrieve']; + put?: never; + /** @description Internal API endpoints for users. */ + post: operations['users_export_token_create']; + /** @description Internal API endpoints for users. */ + delete: operations['users_export_token_destroy']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/forget_number/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** @description Internal API endpoints for users. */ + put: operations['users_forget_number_update']; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/get_backend_verification_code/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for users. */ + get: operations['users_get_backend_verification_code_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/get_telegram_verification_code/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for users. */ + get: operations['users_get_telegram_verification_code_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/get_verification_call/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for users. */ + get: operations['users_get_verification_call_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/get_verification_code/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for users. */ + get: operations['users_get_verification_code_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/make_test_call/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for users. */ + post: operations['users_make_test_call_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/send_test_push/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for users. */ + post: operations['users_send_test_push_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/send_test_sms/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for users. */ + post: operations['users_send_test_sms_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/unlink_backend/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for users. */ + post: operations['users_unlink_backend_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/unlink_slack/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for users. */ + post: operations['users_unlink_slack_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/unlink_telegram/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Internal API endpoints for users. */ + post: operations['users_unlink_telegram_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/upcoming_shifts/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for users. */ + get: operations['users_upcoming_shifts_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{id}/verify_number/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** @description Internal API endpoints for users. */ + put: operations['users_verify_number_update']; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/timezone_options/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Internal API endpoints for users. */ + get: operations['users_timezone_options_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { schemas: { + /** + * @description * `acknowledge` - acknowledge + * * `resolve` - resolve + * * `silence` - silence + * * `restart` - restart + * @enum {string} + */ + ActionEnum: 'acknowledge' | 'resolve' | 'silence' | 'restart'; Alert: { readonly id: string; /** Format: uri */ link_to_upstream_details?: string | null; - readonly render_for_web: string; + readonly render_for_web: { + title: string; + message: string; + image_url: string | null; + source_link: string | null; + }; /** Format: date-time */ readonly created_at: string; }; @@ -467,10 +1155,17 @@ export interface components { /** Format: date-time */ readonly started_at: string; readonly related_users: components['schemas']['UserShort'][]; - readonly render_for_web: components['schemas']['render_for_web']; + readonly render_for_web: + | { + title: string; + message: string; + image_url: string | null; + source_link: string | null; + } + | Record; dependent_alert_groups: components['schemas']['ShortAlertGroup'][]; root_alert_group: components['schemas']['ShortAlertGroup']; - readonly status: string; + readonly status: number; /** @description Generate a link for AlertGroup to declare Grafana Incident by click */ readonly declare_incident_link: string; team: string | null; @@ -509,6 +1204,34 @@ export interface components { important: boolean; }[]; }; + AlertGroupAttach: { + root_alert_group_pk: string; + }; + AlertGroupBulkActionOptions: { + value: components['schemas']['ActionEnum']; + display_name: components['schemas']['ActionEnum']; + }; + AlertGroupBulkActionRequest: { + alert_group_pks: string[]; + action: components['schemas']['ActionEnum']; + /** @description only applicable for silence */ + delay?: number | null; + }; + AlertGroupFilters: { + name: string; + type: string; + href?: string; + global?: boolean; + default?: { + [key: string]: unknown; + }; + description?: string; + options: components['schemas']['AlertGroupFiltersOptions']; + }; + AlertGroupFiltersOptions: { + value: string; + display_name: number; + }; AlertGroupLabel: { key: components['schemas']['Key']; value: components['schemas']['Value']; @@ -537,29 +1260,326 @@ export interface components { /** Format: date-time */ readonly started_at: string; readonly related_users: components['schemas']['UserShort'][]; - readonly render_for_web: components['schemas']['render_for_web']; + readonly render_for_web: + | { + title: string; + message: string; + image_url: string | null; + source_link: string | null; + } + | Record; dependent_alert_groups: components['schemas']['ShortAlertGroup'][]; root_alert_group: components['schemas']['ShortAlertGroup']; - readonly status: string; + readonly status: number; /** @description Generate a link for AlertGroup to declare Grafana Incident by click */ readonly declare_incident_link: string; team: string | null; grafana_incident_id?: string | null; readonly labels: components['schemas']['AlertGroupLabel'][]; }; + AlertGroupResolve: { + resolution_note?: string | null; + }; + AlertGroupSilence: { + delay: number; + }; + AlertGroupSilenceOptions: { + value: components['schemas']['AlertGroupSilenceOptionsValueEnum']; + display_name: components['schemas']['AlertGroupSilenceOptionsDisplayNameEnum']; + }; + /** + * @description * `30 minutes` - 30 minutes + * * `1 hour` - 1 hour + * * `2 hours` - 2 hours + * * `3 hours` - 3 hours + * * `4 hours` - 4 hours + * * `6 hours` - 6 hours + * * `12 hours` - 12 hours + * * `16 hours` - 16 hours + * * `20 hours` - 20 hours + * * `24 hours` - 24 hours + * * `Forever` - Forever + * @enum {string} + */ + AlertGroupSilenceOptionsDisplayNameEnum: + | '30 minutes' + | '1 hour' + | '2 hours' + | '3 hours' + | '4 hours' + | '6 hours' + | '12 hours' + | '16 hours' + | '20 hours' + | '24 hours' + | 'Forever'; + /** + * @description * `1800` - 1800 + * * `3600` - 3600 + * * `7200` - 7200 + * * `10800` - 10800 + * * `14400` - 14400 + * * `21600` - 21600 + * * `43200` - 43200 + * * `57600` - 57600 + * * `72000` - 72000 + * * `86400` - 86400 + * * `-1` - -1 + * @enum {integer} + */ + AlertGroupSilenceOptionsValueEnum: 1800 | 3600 | 7200 | 10800 | 14400 | 21600 | 43200 | 57600 | 72000 | 86400 | -1; AlertGroupStats: { count: number; }; + AlertGroupUnpageUser: { + user_id: string; + }; + AlertReceiveChannel: { + readonly id: string; + readonly description: string | null; + description_short?: string | null; + integration: components['schemas']['IntegrationEnum']; + readonly smile_code: string; + verbal_name?: string | null; + readonly author: string; + readonly organization: string; + team?: string | null; + /** Format: date-time */ + readonly created_at: string; + readonly integration_url: string | null; + readonly alert_count: number; + readonly alert_groups_count: number; + allow_source_based_resolving?: boolean; + readonly instructions: string; + readonly is_able_to_autoresolve: boolean; + readonly default_channel_filter: string | null; + readonly demo_alert_enabled: boolean; + readonly maintenance_mode: + | (components['schemas']['MaintenanceModeEnum'] | components['schemas']['NullEnum']) + | null; + readonly maintenance_till: number | null; + readonly heartbeat: components['schemas']['IntegrationHeartBeat'] | null; + readonly is_available_for_integration_heartbeat: boolean; + readonly allow_delete: boolean; + readonly demo_alert_payload: { + [key: string]: unknown; + }; + readonly routes_count: number; + readonly connected_escalations_chains_count: number; + readonly is_based_on_alertmanager: boolean; + readonly inbound_email: string; + readonly is_legacy: boolean; + labels?: components['schemas']['LabelPair'][]; + alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels']; + /** Format: date-time */ + readonly alertmanager_v2_migrated_at: string | null; + }; + AlertReceiveChannelConnectContactPoint: { + datasource_uid: string; + contact_point_name: string; + }; + AlertReceiveChannelConnectedContactPoints: { + uid: string; + name: string; + contact_points: components['schemas']['AlertReceiveChannelConnectedContactPointsInner'][]; + }; + AlertReceiveChannelConnectedContactPointsInner: { + name: string; + notification_connected: boolean; + }; + AlertReceiveChannelContactPoints: { + uid: string; + name: string; + contact_points: string[]; + }; + AlertReceiveChannelCreateContactPoint: { + datasource_uid: string; + contact_point_name: string; + }; + AlertReceiveChannelDisconnectContactPoint: { + datasource_uid: string; + contact_point_name: string; + }; + AlertReceiveChannelFilters: { + name: string; + display_name?: string; + type: string; + href: string; + global?: boolean; + }; + AlertReceiveChannelIntegrationOptions: { + value: string; + display_name: string; + short_description: string; + featured: boolean; + featured_tag_name: string | null; + }; + AlertReceiveChannelPolymorphic: + | components['schemas']['AlertReceiveChannel'] + | components['schemas']['FilterAlertReceiveChannel']; + AlertReceiveChannelSendDemoAlert: { + demo_alert_payload?: { + [key: string]: unknown; + } | null; + }; + AlertReceiveChannelStartMaintenance: { + mode: components['schemas']['ModeEnum']; + duration: components['schemas']['DurationEnum']; + }; + AlertReceiveChannelUpdate: { + readonly id: string; + readonly description: string | null; + description_short?: string | null; + readonly integration: components['schemas']['IntegrationEnum']; + readonly smile_code: string; + verbal_name?: string | null; + readonly author: string; + readonly organization: string; + team?: string | null; + /** Format: date-time */ + readonly created_at: string; + readonly integration_url: string | null; + readonly alert_count: number; + readonly alert_groups_count: number; + allow_source_based_resolving?: boolean; + readonly instructions: string; + readonly is_able_to_autoresolve: boolean; + readonly default_channel_filter: string | null; + readonly demo_alert_enabled: boolean; + readonly maintenance_mode: + | (components['schemas']['MaintenanceModeEnum'] | components['schemas']['NullEnum']) + | null; + readonly maintenance_till: number | null; + readonly heartbeat: components['schemas']['IntegrationHeartBeat'] | null; + readonly is_available_for_integration_heartbeat: boolean; + readonly allow_delete: boolean; + readonly demo_alert_payload: { + [key: string]: unknown; + }; + readonly routes_count: number; + readonly connected_escalations_chains_count: number; + readonly is_based_on_alertmanager: boolean; + readonly inbound_email: string; + readonly is_legacy: boolean; + labels?: components['schemas']['LabelPair'][]; + alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels']; + /** Format: date-time */ + readonly alertmanager_v2_migrated_at: string | null; + }; + /** @enum {integer} */ + CloudConnectionStatusEnum: 0 | 1 | 2 | 3; + /** @description This serializer is consistent with apps.api.serializers.labels.LabelPairSerializer, but allows null for value ID. */ + CustomLabel: { + key: components['schemas']['CustomLabelKey']; + value: components['schemas']['CustomLabelValue']; + }; + CustomLabelKey: { + id: string; + name: string; + /** @default false */ + prescribed: boolean; + }; + CustomLabelValue: { + id: string | null; + name: string; + /** @default false */ + prescribed: boolean; + }; + /** + * @description * `3600` - 3600 + * * `10800` - 10800 + * * `21600` - 21600 + * * `43200` - 43200 + * * `86400` - 86400 + * @enum {integer} + */ + DurationEnum: 3600 | 10800 | 21600 | 43200 | 86400; FastAlertReceiveChannel: { readonly id: string; readonly integration: string; verbal_name?: string | null; readonly deleted: boolean; }; + FastOrganization: { + readonly pk: string; + readonly name: string; + }; + FastTeam: { + readonly id: string; + name: string; + email?: string | null; + /** Format: uri */ + avatar_url: string; + }; FastUser: { pk: string; readonly username: string; }; + FilterAlertReceiveChannel: { + readonly value: string; + readonly display_name: string; + readonly integration_url: string | null; + }; + FilterUser: { + value: string; + display_name: string; + }; + /** @description Alert group labels configuration for the integration. See AlertReceiveChannel.alert_group_labels for details. */ + IntegrationAlertGroupLabels: { + inheritable: { + [key: string]: boolean | undefined; + }; + custom: components['schemas']['CustomLabel'][]; + template: string | null; + }; + /** + * @description * `alertmanager` - Alertmanager + * * `legacy_alertmanager` - (Legacy) AlertManager + * * `grafana` - Grafana + * * `grafana_alerting` - Grafana Alerting + * * `legacy_grafana_alerting` - (Legacy) Grafana Alerting + * * `formatted_webhook` - Formatted webhook + * * `webhook` - Webhook + * * `kapacitor` - Kapacitor + * * `elastalert` - Elastalert + * * `heartbeat` - Heartbeat + * * `inbound_email` - Inbound Email + * * `maintenance` - Maintenance + * * `manual` - Manual + * * `slack_channel` - Slack Channel + * * `zabbix` - Zabbix + * * `direct_paging` - Direct paging + * @enum {string} + */ + IntegrationEnum: + | 'alertmanager' + | 'legacy_alertmanager' + | 'grafana' + | 'grafana_alerting' + | 'legacy_grafana_alerting' + | 'formatted_webhook' + | 'webhook' + | 'kapacitor' + | 'elastalert' + | 'heartbeat' + | 'inbound_email' + | 'maintenance' + | 'manual' + | 'slack_channel' + | 'zabbix' + | 'direct_paging'; + IntegrationHeartBeat: { + readonly id: string; + timeout_seconds: components['schemas']['TimeoutSecondsEnum']; + alert_receive_channel: string; + readonly link: string | null; + readonly last_heartbeat_time_verbal: string | null; + /** @description 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. */ + readonly status: boolean; + readonly instruction: string; + }; Key: { id: string; name: string; @@ -571,29 +1591,201 @@ export interface components { LabelKey: { id: string; name: string; - prescribed?: boolean; + /** @default false */ + prescribed: boolean; }; - LabelKeyValues: { + LabelOption: { key: components['schemas']['LabelKey']; values: components['schemas']['LabelValue'][]; }; + LabelPair: { + key: components['schemas']['LabelKey']; + value: components['schemas']['LabelValue']; + }; LabelRepr: { name: string; }; LabelValue: { id: string; name: string; - prescribed?: boolean; + /** @default false */ + prescribed: boolean; }; + ListUser: { + readonly pk: string; + readonly organization: components['schemas']['FastOrganization']; + current_team?: string | null; + /** Format: email */ + readonly email: string; + readonly username: string; + readonly name: string; + readonly role: components['schemas']['RoleEnum']; + /** Format: uri */ + readonly avatar: string; + /** Format: uri */ + readonly avatar_full: string; + timezone?: string | null; + working_hours?: components['schemas']['WorkingHours']; + unverified_phone_number?: string | null; + /** @description Use property to highlight that _verified_phone_number should not be modified directly */ + readonly verified_phone_number: string | null; + readonly slack_user_identity: components['schemas']['SlackUserIdentity']; + readonly telegram_configuration: components['schemas']['TelegramToUserConnector']; + readonly messaging_backends: { + [key: string]: + | { + [key: string]: unknown; + } + | undefined; + }; + readonly notification_chain_verbal: { + default: string; + important: string; + }; + readonly cloud_connection_status: components['schemas']['CloudConnectionStatusEnum'] | null; + hide_phone_number?: boolean; + }; + /** + * @description * `0` - Debug + * * `1` - Maintenance + * @enum {integer} + */ + MaintenanceModeEnum: 0 | 1; + /** + * @description * `0` - Debug + * * `1` - Maintenance + * @enum {integer} + */ + ModeEnum: 0 | 1; + /** @enum {unknown} */ + NullEnum: null; PaginatedAlertGroupListList: { next?: string | null; previous?: string | null; results?: components['schemas']['AlertGroupList'][]; + page_size?: number; }; - Paginatedsilence_optionsList: { + PaginatedAlertReceiveChannelPolymorphicList: { + /** @example 123 */ + count?: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=4 + */ next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=2 + */ previous?: string | null; - results?: components['schemas']['silence_options'][]; + results?: components['schemas']['AlertReceiveChannelPolymorphic'][]; + page_size?: number; + current_page_number?: number; + total_pages?: number; + }; + PaginatedUserPolymorphicList: { + /** @example 123 */ + count?: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=4 + */ + next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=2 + */ + previous?: string | null; + results?: components['schemas']['UserPolymorphic'][]; + page_size?: number; + current_page_number?: number; + total_pages?: number; + }; + PatchedAlertReceiveChannelUpdate: { + readonly id?: string; + readonly description?: string | null; + description_short?: string | null; + readonly integration?: components['schemas']['IntegrationEnum']; + readonly smile_code?: string; + verbal_name?: string | null; + readonly author?: string; + readonly organization?: string; + team?: string | null; + /** Format: date-time */ + readonly created_at?: string; + readonly integration_url?: string | null; + readonly alert_count?: number; + readonly alert_groups_count?: number; + allow_source_based_resolving?: boolean; + readonly instructions?: string; + readonly is_able_to_autoresolve?: boolean; + readonly default_channel_filter?: string | null; + readonly demo_alert_enabled?: boolean; + readonly maintenance_mode?: + | (components['schemas']['MaintenanceModeEnum'] | components['schemas']['NullEnum']) + | null; + readonly maintenance_till?: number | null; + readonly heartbeat?: components['schemas']['IntegrationHeartBeat'] | null; + readonly is_available_for_integration_heartbeat?: boolean; + readonly allow_delete?: boolean; + readonly demo_alert_payload?: { + [key: string]: unknown; + }; + readonly routes_count?: number; + readonly connected_escalations_chains_count?: number; + readonly is_based_on_alertmanager?: boolean; + readonly inbound_email?: string; + readonly is_legacy?: boolean; + labels?: components['schemas']['LabelPair'][]; + alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels']; + /** Format: date-time */ + readonly alertmanager_v2_migrated_at?: string | null; + }; + PatchedUser: { + readonly pk?: string; + readonly organization?: components['schemas']['FastOrganization']; + current_team?: string | null; + /** Format: email */ + readonly email?: string; + readonly username?: string; + readonly name?: string; + readonly role?: components['schemas']['RoleEnum']; + /** Format: uri */ + readonly avatar?: string; + /** Format: uri */ + readonly avatar_full?: string; + timezone?: string | null; + working_hours?: components['schemas']['WorkingHours']; + unverified_phone_number?: string | null; + /** @description Use property to highlight that _verified_phone_number should not be modified directly */ + readonly verified_phone_number?: string | null; + readonly slack_user_identity?: components['schemas']['SlackUserIdentity']; + readonly telegram_configuration?: components['schemas']['TelegramToUserConnector']; + readonly messaging_backends?: { + [key: string]: + | { + [key: string]: unknown; + } + | undefined; + }; + readonly notification_chain_verbal?: { + default: string; + important: string; + }; + readonly cloud_connection_status?: components['schemas']['CloudConnectionStatusEnum'] | null; + hide_phone_number?: boolean; + readonly is_currently_oncall?: boolean; + }; + PreviewTemplateRequest: { + template_body?: string | null; + template_name?: string | null; + payload?: { + [key: string]: unknown; + } | null; + }; + PreviewTemplateResponse: { + preview: string | null; + is_valid_json_object: boolean; }; /** * @description * `0` - source @@ -607,12 +1799,119 @@ export interface components { * @enum {integer} */ ResolvedByEnum: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + /** + * @description * `0` - ADMIN + * * `1` - EDITOR + * * `2` - VIEWER + * * `3` - NONE + * @enum {integer} + */ + RoleEnum: 0 | 1 | 2 | 3; ShortAlertGroup: { readonly pk: string; - readonly render_for_web: components['schemas']['render_for_web']; + readonly render_for_web: + | { + title: string; + message: string; + image_url: string | null; + source_link: string | null; + } + | Record; alert_receive_channel: components['schemas']['FastAlertReceiveChannel']; readonly inside_organization_number: number; }; + SlackUserIdentity: { + readonly slack_login: string; + readonly slack_id: string; + readonly avatar: string; + readonly name: string; + readonly display_name: string | null; + }; + TelegramToUserConnector: { + telegram_nick_name?: string | null; + /** Format: int64 */ + telegram_chat_id: number; + }; + /** + * @description * `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 + * @enum {integer} + */ + TimeoutSecondsEnum: 60 | 120 | 180 | 300 | 600 | 900 | 1800 | 3600 | 43200 | 86400; + User: { + readonly pk: string; + readonly organization: components['schemas']['FastOrganization']; + current_team?: string | null; + /** Format: email */ + readonly email: string; + readonly username: string; + readonly name: string; + readonly role: components['schemas']['RoleEnum']; + /** Format: uri */ + readonly avatar: string; + /** Format: uri */ + readonly avatar_full: string; + timezone?: string | null; + working_hours?: components['schemas']['WorkingHours']; + unverified_phone_number?: string | null; + /** @description Use property to highlight that _verified_phone_number should not be modified directly */ + readonly verified_phone_number: string | null; + readonly slack_user_identity: components['schemas']['SlackUserIdentity']; + readonly telegram_configuration: components['schemas']['TelegramToUserConnector']; + readonly messaging_backends: { + [key: string]: + | { + [key: string]: unknown; + } + | undefined; + }; + readonly notification_chain_verbal: { + default: string; + important: string; + }; + readonly cloud_connection_status: components['schemas']['CloudConnectionStatusEnum'] | null; + hide_phone_number?: boolean; + readonly is_currently_oncall: boolean; + }; + UserExportTokenGetResponse: { + /** Format: date-time */ + created_at: string; + /** Format: date-time */ + revoked_at: string | null; + active: boolean; + }; + UserExportTokenPostResponse: { + token: string; + /** Format: date-time */ + created_at: string; + export_url: string; + }; + UserGetTelegramVerificationCode: { + telegram_code: string; + bot_link: string; + }; + UserIsCurrentlyOnCall: { + username: string; + pk: string; + avatar: string; + avatar_full: string; + name: string; + readonly timezone: string | null; + readonly teams: components['schemas']['FastTeam'][]; + readonly is_currently_oncall: boolean; + }; + UserPolymorphic: + | components['schemas']['FilterUser'] + | components['schemas']['UserIsCurrentlyOnCall'] + | components['schemas']['ListUser']; UserShort: { username: string; pk: string; @@ -623,15 +1922,18 @@ export interface components { id: string; name: string; }; - render_for_web: { - title: string; - message: string; - image_url: string; - source_link: string; + WorkingHours: { + monday: components['schemas']['WorkingHoursPeriod'][]; + tuesday: components['schemas']['WorkingHoursPeriod'][]; + wednesday: components['schemas']['WorkingHoursPeriod'][]; + thursday: components['schemas']['WorkingHoursPeriod'][]; + friday: components['schemas']['WorkingHoursPeriod'][]; + saturday: components['schemas']['WorkingHoursPeriod'][]; + sunday: components['schemas']['WorkingHoursPeriod'][]; }; - silence_options: { - value: string; - display_name: string; + WorkingHoursPeriod: { + start: string; + end: string; }; }; responses: never; @@ -642,15 +1944,643 @@ export interface components { } export type $defs = Record; export interface operations { - alertgroups_list: { + alert_receive_channels_list: { parameters: { query?: { - /** @description The pagination cursor value. */ - cursor?: string; + /** @description * `alertmanager` - Alertmanager + * * `legacy_alertmanager` - (Legacy) AlertManager + * * `grafana` - Grafana + * * `grafana_alerting` - Grafana Alerting + * * `legacy_grafana_alerting` - (Legacy) Grafana Alerting + * * `formatted_webhook` - Formatted webhook + * * `webhook` - Webhook + * * `kapacitor` - Kapacitor + * * `elastalert` - Elastalert + * * `heartbeat` - Heartbeat + * * `inbound_email` - Inbound Email + * * `maintenance` - Maintenance + * * `manual` - Manual + * * `slack_channel` - Slack Channel + * * `zabbix` - Zabbix + * * `direct_paging` - Direct paging */ + integration?: ( + | 'alertmanager' + | 'direct_paging' + | 'elastalert' + | 'formatted_webhook' + | 'grafana' + | 'grafana_alerting' + | 'heartbeat' + | 'inbound_email' + | 'kapacitor' + | 'legacy_alertmanager' + | 'legacy_grafana_alerting' + | 'maintenance' + | 'manual' + | 'slack_channel' + | 'webhook' + | 'zabbix' + )[]; + /** @description * `alertmanager` - Alertmanager + * * `legacy_alertmanager` - (Legacy) AlertManager + * * `grafana` - Grafana + * * `grafana_alerting` - Grafana Alerting + * * `legacy_grafana_alerting` - (Legacy) Grafana Alerting + * * `formatted_webhook` - Formatted webhook + * * `webhook` - Webhook + * * `kapacitor` - Kapacitor + * * `elastalert` - Elastalert + * * `heartbeat` - Heartbeat + * * `inbound_email` - Inbound Email + * * `maintenance` - Maintenance + * * `manual` - Manual + * * `slack_channel` - Slack Channel + * * `zabbix` - Zabbix + * * `direct_paging` - Direct paging */ + integration_ne?: ( + | 'alertmanager' + | 'direct_paging' + | 'elastalert' + | 'formatted_webhook' + | 'grafana' + | 'grafana_alerting' + | 'heartbeat' + | 'inbound_email' + | 'kapacitor' + | 'legacy_alertmanager' + | 'legacy_grafana_alerting' + | 'maintenance' + | 'manual' + | 'slack_channel' + | 'webhook' + | 'zabbix' + )[]; + /** @description * `0` - Debug + * * `1` - Maintenance */ + maintenance_mode?: (0 | 1)[]; + /** @description A page number within the paginated result set. */ + page?: number; /** @description Number of results to return per page. */ perpage?: number; /** @description A search term. */ search?: string; + team?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PaginatedAlertReceiveChannelPolymorphicList']; + }; + }; + }; + }; + alert_receive_channels_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertReceiveChannel']; + 'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannel']; + 'multipart/form-data': components['schemas']['AlertReceiveChannel']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertReceiveChannel']; + }; + }; + }; + }; + alert_receive_channels_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertReceiveChannel']; + }; + }; + }; + }; + alert_receive_channels_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['AlertReceiveChannelUpdate']; + 'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannelUpdate']; + 'multipart/form-data': components['schemas']['AlertReceiveChannelUpdate']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertReceiveChannelUpdate']; + }; + }; + }; + }; + alert_receive_channels_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alert_receive_channels_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedAlertReceiveChannelUpdate']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedAlertReceiveChannelUpdate']; + 'multipart/form-data': components['schemas']['PatchedAlertReceiveChannelUpdate']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertReceiveChannelUpdate']; + }; + }; + }; + }; + alert_receive_channels_change_team_update: { + parameters: { + query: { + team_id: string; + }; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alert_receive_channels_connect_contact_point_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertReceiveChannelConnectContactPoint']; + 'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannelConnectContactPoint']; + 'multipart/form-data': components['schemas']['AlertReceiveChannelConnectContactPoint']; + }; + }; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alert_receive_channels_connected_contact_points_list: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertReceiveChannelConnectedContactPoints'][]; + }; + }; + }; + }; + alert_receive_channels_counters_per_integration_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: + | { + alerts_count: number; + alert_groups_count: number; + } + | undefined; + }; + }; + }; + }; + }; + alert_receive_channels_create_contact_point_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertReceiveChannelCreateContactPoint']; + 'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannelCreateContactPoint']; + 'multipart/form-data': components['schemas']['AlertReceiveChannelCreateContactPoint']; + }; + }; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alert_receive_channels_disconnect_contact_point_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertReceiveChannelDisconnectContactPoint']; + 'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannelDisconnectContactPoint']; + 'multipart/form-data': components['schemas']['AlertReceiveChannelDisconnectContactPoint']; + }; + }; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alert_receive_channels_migrate_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alert_receive_channels_preview_template_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PreviewTemplateRequest']; + 'application/x-www-form-urlencoded': components['schemas']['PreviewTemplateRequest']; + 'multipart/form-data': components['schemas']['PreviewTemplateRequest']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PreviewTemplateResponse']; + }; + }; + }; + }; + alert_receive_channels_send_demo_alert_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['AlertReceiveChannelSendDemoAlert']; + 'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannelSendDemoAlert']; + 'multipart/form-data': components['schemas']['AlertReceiveChannelSendDemoAlert']; + }; + }; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alert_receive_channels_start_maintenance_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertReceiveChannelStartMaintenance']; + 'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannelStartMaintenance']; + 'multipart/form-data': components['schemas']['AlertReceiveChannelStartMaintenance']; + }; + }; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alert_receive_channels_stop_maintenance_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert receive channel. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alert_receive_channels_contact_points_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertReceiveChannelContactPoints'][]; + }; + }; + }; + }; + alert_receive_channels_counters_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: + | { + alerts_count: number; + alert_groups_count: number; + } + | undefined; + }; + }; + }; + }; + }; + alert_receive_channels_filters_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertReceiveChannelFilters'][]; + }; + }; + }; + }; + alert_receive_channels_integration_options_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertReceiveChannelIntegrationOptions'][]; + }; + }; + }; + }; + alert_receive_channels_validate_name_retrieve: { + parameters: { + query: { + verbal_name: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No response body */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alertgroups_list: { + parameters: { + query?: { + acknowledged_by?: string[]; + /** @description The pagination cursor value. */ + cursor?: string; + escalation_chain?: string[]; + integration?: string[]; + invitees_are?: string[]; + involved_users_are?: string[]; + is_root?: boolean; + mine?: boolean; + /** @description Number of results to return per page. */ + perpage?: number; + resolved_at?: string; + resolved_by?: string[]; + /** @description A search term. */ + search?: string; + silenced_by?: string[]; + started_at?: string; + /** @description * `0` - New + * * `1` - Acknowledged + * * `2` - Resolved + * * `3` - Silenced */ + status?: (0 | 1 | 2 | 3)[]; + with_resolution_note?: boolean; }; header?: never; path?: never; @@ -673,6 +2603,7 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; @@ -694,6 +2625,7 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; @@ -714,17 +2646,12 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; - }; - }; + requestBody?: never; responses: { 200: { headers: { @@ -741,15 +2668,16 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; requestBody: { content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; + 'application/json': components['schemas']['AlertGroupAttach']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroupAttach']; + 'multipart/form-data': components['schemas']['AlertGroupAttach']; }; }; responses: { @@ -763,20 +2691,42 @@ export interface operations { }; }; }; + alertgroups_escalation_snapshot_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this alert group. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; alertgroups_preview_template_create: { parameters: { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; - requestBody: { + requestBody?: { content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; + 'application/json': components['schemas']['PreviewTemplateRequest']; + 'application/x-www-form-urlencoded': components['schemas']['PreviewTemplateRequest']; + 'multipart/form-data': components['schemas']['PreviewTemplateRequest']; }; }; responses: { @@ -785,7 +2735,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['AlertGroup']; + 'application/json': components['schemas']['PreviewTemplateResponse']; }; }; }; @@ -795,15 +2745,16 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; - requestBody: { + requestBody?: { content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; + 'application/json': components['schemas']['AlertGroupResolve']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroupResolve']; + 'multipart/form-data': components['schemas']['AlertGroupResolve']; }; }; responses: { @@ -822,15 +2773,16 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; requestBody: { content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; + 'application/json': components['schemas']['AlertGroupSilence']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroupSilence']; + 'multipart/form-data': components['schemas']['AlertGroupSilence']; }; }; responses: { @@ -849,17 +2801,12 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; - }; - }; + requestBody?: never; responses: { 200: { headers: { @@ -876,17 +2823,12 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; - }; - }; + requestBody?: never; responses: { 200: { headers: { @@ -903,15 +2845,16 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; requestBody: { content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; + 'application/json': components['schemas']['AlertGroupUnpageUser']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroupUnpageUser']; + 'multipart/form-data': components['schemas']['AlertGroupUnpageUser']; }; }; responses: { @@ -930,17 +2873,12 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; - }; - }; + requestBody?: never; responses: { 200: { headers: { @@ -957,17 +2895,12 @@ export interface operations { query?: never; header?: never; path: { + /** @description A string identifying this alert group. */ id: string; }; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; - }; - }; + requestBody?: never; responses: { 200: { headers: { @@ -988,23 +2921,22 @@ export interface operations { }; requestBody: { content: { - 'application/json': components['schemas']['AlertGroup']; - 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; - 'multipart/form-data': components['schemas']['AlertGroup']; + 'application/json': components['schemas']['AlertGroupBulkActionRequest']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroupBulkActionRequest']; + 'multipart/form-data': components['schemas']['AlertGroupBulkActionRequest']; }; }; responses: { + /** @description No response body */ 200: { headers: { [name: string]: unknown; }; - content: { - 'application/json': components['schemas']['AlertGroup']; - }; + content?: never; }; }; }; - alertgroups_bulk_action_options_retrieve: { + alertgroups_bulk_action_options_list: { parameters: { query?: never; header?: never; @@ -1018,12 +2950,12 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['AlertGroup']; + 'application/json': components['schemas']['AlertGroupBulkActionOptions'][]; }; }; }; }; - alertgroups_filters_retrieve: { + alertgroups_filters_list: { parameters: { query?: never; header?: never; @@ -1037,7 +2969,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['AlertGroup']; + 'application/json': components['schemas']['AlertGroupFilters'][]; }; }; }; @@ -1058,7 +2990,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['LabelKeyValues']; + 'application/json': components['schemas']['LabelOption']; }; }; }; @@ -1082,16 +3014,9 @@ export interface operations { }; }; }; - alertgroups_silence_options_list: { + alertgroups_silence_options_retrieve: { parameters: { - query?: { - /** @description The pagination cursor value. */ - cursor?: string; - /** @description Number of results to return per page. */ - perpage?: number; - /** @description A search term. */ - search?: string; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -1103,14 +3028,34 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['Paginatedsilence_optionsList']; + 'application/json': components['schemas']['AlertGroupSilenceOptions']; }; }; }; }; alertgroups_stats_retrieve: { parameters: { - query?: never; + query?: { + acknowledged_by?: string[]; + escalation_chain?: string[]; + integration?: string[]; + invitees_are?: string[]; + involved_users_are?: string[]; + is_root?: boolean; + mine?: boolean; + resolved_at?: string; + resolved_by?: string[]; + /** @description A search term. */ + search?: string; + silenced_by?: string[]; + started_at?: string; + /** @description * `0` - New + * * `1` - Acknowledged + * * `2` - Resolved + * * `3` - Silenced */ + status?: (0 | 1 | 2 | 3)[]; + with_resolution_note?: boolean; + }; header?: never; path?: never; cookie?: never; @@ -1141,9 +3086,16 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': { - [key: string]: unknown; - }; + 'application/json': ( + | 'msteams' + | 'slack' + | 'telegram' + | 'live_settings' + | 'grafana_cloud_notifications' + | 'grafana_cloud_connection' + | 'grafana_alerting_v2' + | 'labels' + )[]; }; }; }; @@ -1168,7 +3120,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['LabelKeyValues']; + 'application/json': components['schemas']['LabelOption']; }; }; }; @@ -1189,7 +3141,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['LabelKeyValues']; + 'application/json': components['schemas']['LabelOption']; }; }; }; @@ -1216,7 +3168,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['LabelKeyValues']; + 'application/json': components['schemas']['LabelOption']; }; }; }; @@ -1243,7 +3195,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['LabelKeyValues']; + 'application/json': components['schemas']['LabelOption']; }; }; }; @@ -1293,7 +3245,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['LabelKeyValues']; + 'application/json': components['schemas']['LabelOption']; }; }; }; @@ -1317,4 +3269,609 @@ export interface operations { }; }; }; + users_list: { + parameters: { + query?: { + email?: string; + /** @description A page number within the paginated result set. */ + page?: number; + /** @description * `grafana-oncall-app.alert-groups:direct-paging` - ALERT_GROUPS_DIRECT_PAGING + * * `grafana-oncall-app.alert-groups:read` - ALERT_GROUPS_READ + * * `grafana-oncall-app.alert-groups:write` - ALERT_GROUPS_WRITE + * * `grafana-oncall-app.api-keys:read` - API_KEYS_READ + * * `grafana-oncall-app.api-keys:write` - API_KEYS_WRITE + * * `grafana-oncall-app.chatops:read` - CHATOPS_READ + * * `grafana-oncall-app.chatops:update-settings` - CHATOPS_UPDATE_SETTINGS + * * `grafana-oncall-app.chatops:write` - CHATOPS_WRITE + * * `grafana-oncall-app.escalation-chains:read` - ESCALATION_CHAINS_READ + * * `grafana-oncall-app.escalation-chains:write` - ESCALATION_CHAINS_WRITE + * * `grafana-oncall-app.integrations:read` - INTEGRATIONS_READ + * * `grafana-oncall-app.integrations:test` - INTEGRATIONS_TEST + * * `grafana-oncall-app.integrations:write` - INTEGRATIONS_WRITE + * * `grafana-oncall-app.maintenance:read` - MAINTENANCE_READ + * * `grafana-oncall-app.maintenance:write` - MAINTENANCE_WRITE + * * `grafana-oncall-app.notifications:read` - NOTIFICATIONS_READ + * * `grafana-oncall-app.notification-settings:read` - NOTIFICATION_SETTINGS_READ + * * `grafana-oncall-app.notification-settings:write` - NOTIFICATION_SETTINGS_WRITE + * * `grafana-oncall-app.other-settings:read` - OTHER_SETTINGS_READ + * * `grafana-oncall-app.other-settings:write` - OTHER_SETTINGS_WRITE + * * `grafana-oncall-app.outgoing-webhooks:read` - OUTGOING_WEBHOOKS_READ + * * `grafana-oncall-app.outgoing-webhooks:write` - OUTGOING_WEBHOOKS_WRITE + * * `grafana-oncall-app.schedules:export` - SCHEDULES_EXPORT + * * `grafana-oncall-app.schedules:read` - SCHEDULES_READ + * * `grafana-oncall-app.schedules:write` - SCHEDULES_WRITE + * * `grafana-oncall-app.user-settings:admin` - USER_SETTINGS_ADMIN + * * `grafana-oncall-app.user-settings:read` - USER_SETTINGS_READ + * * `grafana-oncall-app.user-settings:write` - USER_SETTINGS_WRITE */ + permission?: + | 'grafana-oncall-app.alert-groups:direct-paging' + | 'grafana-oncall-app.alert-groups:read' + | 'grafana-oncall-app.alert-groups:write' + | 'grafana-oncall-app.api-keys:read' + | 'grafana-oncall-app.api-keys:write' + | 'grafana-oncall-app.chatops:read' + | 'grafana-oncall-app.chatops:update-settings' + | 'grafana-oncall-app.chatops:write' + | 'grafana-oncall-app.escalation-chains:read' + | 'grafana-oncall-app.escalation-chains:write' + | 'grafana-oncall-app.integrations:read' + | 'grafana-oncall-app.integrations:test' + | 'grafana-oncall-app.integrations:write' + | 'grafana-oncall-app.maintenance:read' + | 'grafana-oncall-app.maintenance:write' + | 'grafana-oncall-app.notification-settings:read' + | 'grafana-oncall-app.notification-settings:write' + | 'grafana-oncall-app.notifications:read' + | 'grafana-oncall-app.other-settings:read' + | 'grafana-oncall-app.other-settings:write' + | 'grafana-oncall-app.outgoing-webhooks:read' + | 'grafana-oncall-app.outgoing-webhooks:write' + | 'grafana-oncall-app.schedules:export' + | 'grafana-oncall-app.schedules:read' + | 'grafana-oncall-app.schedules:write' + | 'grafana-oncall-app.user-settings:admin' + | 'grafana-oncall-app.user-settings:read' + | 'grafana-oncall-app.user-settings:write'; + /** @description Number of results to return per page. */ + perpage?: number; + /** @description * `0` - ADMIN + * * `1` - EDITOR + * * `2` - VIEWER + * * `3` - NONE */ + roles?: (0 | 1 | 2 | 3)[]; + /** @description A search term. */ + search?: string; + team?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PaginatedUserPolymorphicList']; + }; + }; + }; + }; + users_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedUser']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedUser']; + 'multipart/form-data': components['schemas']['PatchedUser']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_export_token_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['UserExportTokenGetResponse']; + }; + }; + }; + }; + users_export_token_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['UserExportTokenPostResponse']; + }; + }; + }; + }; + users_export_token_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_forget_number_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_get_backend_verification_code_retrieve: { + parameters: { + query: { + backend: string; + }; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_get_telegram_verification_code_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['UserGetTelegramVerificationCode']; + }; + }; + }; + }; + users_get_verification_call_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_get_verification_code_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_make_test_call_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_send_test_push_create: { + parameters: { + query?: { + critical?: boolean; + }; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_send_test_sms_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_unlink_backend_create: { + parameters: { + query: { + backend: string; + }; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_unlink_slack_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_unlink_telegram_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_upcoming_shifts_retrieve: { + parameters: { + query?: { + days?: number; + }; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + schedule_id: string; + schedule_name: string; + is_oncall: boolean; + current_shift: { + all_day: boolean; + /** Format: date-time */ + start: string; + /** Format: date-time */ + end: string; + users: { + display_name: string; + pk: string; + email: string; + avatar_full: string; + swap_request: { + pk: string; + user: { + display_name: string; + pk: string; + email: string; + avatar_full: string; + } | null; + } | null; + }[]; + missing_users: string[]; + priority_level: number | null; + source: string | null; + calendar_type: number | null; + is_empty: boolean; + is_gap: boolean; + is_override: boolean; + shift: { + pk: string; + }; + } | null; + next_shift: { + all_day: boolean; + /** Format: date-time */ + start: string; + /** Format: date-time */ + end: string; + users: { + display_name: string; + pk: string; + email: string; + avatar_full: string; + swap_request: { + pk: string; + user: { + display_name: string; + pk: string; + email: string; + avatar_full: string; + } | null; + } | null; + }[]; + missing_users: string[]; + priority_level: number | null; + source: string | null; + calendar_type: number | null; + is_empty: boolean; + is_gap: boolean; + is_override: boolean; + shift: { + pk: string; + }; + } | null; + }[]; + }; + }; + }; + }; + users_verify_number_update: { + parameters: { + query: { + token: string; + }; + header?: never; + path: { + /** @description A string identifying this user. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_timezone_options_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': string[]; + }; + }; + }; + }; } diff --git a/grafana-plugin/src/network/oncall-api/http-client.test.ts b/grafana-plugin/src/network/oncall-api/http-client.test.ts index f33b5c1f..0cdccc8a 100644 --- a/grafana-plugin/src/network/oncall-api/http-client.test.ts +++ b/grafana-plugin/src/network/oncall-api/http-client.test.ts @@ -2,7 +2,7 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { FaroHelper } from 'utils/faro'; -import { customFetch } from './http-client'; +import { getCustomFetchFn } from './http-client'; jest.mock('utils/faro', () => ({ __esModule: true, @@ -31,6 +31,7 @@ const REQUEST_CONFIG = { const URL = 'https://someurl.com'; const SUCCESSFUL_RESPONSE_MOCK = { ok: true }; const ERROR_MOCK = 'error'; +const customFetch = getCustomFetchFn({ withGlobalErrorHandler: true }); describe('customFetch', () => { beforeAll(() => { @@ -54,8 +55,8 @@ describe('customFetch', () => { describe('if response is not successful', () => { it('should push event and error to faro', async () => { (FaroHelper.faro.api.getOTEL as unknown as jest.Mock).mockReturnValueOnce(undefined); - fetchMock.mockRejectedValueOnce(ERROR_MOCK); - await expect(customFetch(URL, REQUEST_CONFIG)).rejects.toEqual(Error(ERROR_MOCK)); + fetchMock.mockResolvedValueOnce({ ok: false, json: () => ERROR_MOCK }); + await expect(customFetch(URL, REQUEST_CONFIG)).rejects.toEqual(ERROR_MOCK); expect(FaroHelper.faro.api.pushEvent).toHaveBeenCalledWith('Request failed', { url: URL }); expect(FaroHelper.faro.api.pushError).toHaveBeenCalledWith(ERROR_MOCK); }); @@ -113,7 +114,7 @@ describe('customFetch', () => { describe('if response is not successful', () => { it('should reject Promise, push event to faro, set span status to error and end span', async () => { - fetchMock.mockRejectedValueOnce(ERROR_MOCK); + fetchMock.mockResolvedValueOnce({ ok: false, json: () => ERROR_MOCK }); await expect(customFetch(URL, REQUEST_CONFIG)).rejects.toEqual(ERROR_MOCK); expect(FaroHelper.faro.api.pushEvent).toHaveBeenCalledWith('Request failed', { url: URL }); expect(FaroHelper.faro.api.pushError).toHaveBeenCalledWith(ERROR_MOCK); diff --git a/grafana-plugin/src/network/oncall-api/http-client.ts b/grafana-plugin/src/network/oncall-api/http-client.ts index 199ee35c..b105d4d7 100644 --- a/grafana-plugin/src/network/oncall-api/http-client.ts +++ b/grafana-plugin/src/network/oncall-api/http-client.ts @@ -4,75 +4,108 @@ import createClient from 'openapi-fetch'; import qs from 'query-string'; import { FaroHelper } from 'utils/faro'; +import { formatBackendError, openErrorNotification } from 'utils/utils'; import { paths } from './autogenerated-api.types'; export const API_PROXY_PREFIX = 'api/plugin-proxy/grafana-oncall-app'; export const API_PATH_PREFIX = '/api/internal/v1'; -export const customFetch = async (url: string, requestConfig: Parameters[1] = {}): Promise => { - const { faro } = FaroHelper; - const otel = faro?.api?.getOTEL(); - - if (faro && otel) { - const tracer = otel.trace.getTracer('default'); - let span = otel.trace.getActiveSpan(); - - if (!span) { - span = tracer.startSpan('http-request'); - span.setAttribute('page_url', document.URL.split('//')[1]); - span.setAttribute(SemanticAttributes.HTTP_URL, url); - span.setAttribute(SemanticAttributes.HTTP_METHOD, requestConfig.method); - } - - return new Promise((resolve, reject) => { - otel.context.with(otel.trace.setSpan(otel.context.active(), span), async () => { - faro.api.pushEvent('Sending request', { url }); - - try { - const response = await fetch(url, { - ...requestConfig, - headers: { - ...requestConfig.headers, - /** - * In short, this header will tell the Grafana plugin proxy, a Go service which use Go's HTTP Transport, - * to retry POST requests (and other non-idempotent requests). This doesn't necessarily make these requests - * idempotent, but it will make them retry-able from Go's (read: net/http) perspective. - * - * https://stackoverflow.com/questions/42847294/how-to-catch-http-server-closed-idle-connection-error/62292758#62292758 - * https://raintank-corp.slack.com/archives/C01C4K8DETW/p1692280544382739?thread_ts=1692279329.797149&cid=C01C4K8DETW - */ 'X-Idempotency-Key': `${Date.now()}-${Math.random()}`, - }, - }); - faro.api.pushEvent('Request completed', { url }); - span.end(); - resolve(response); - } catch (error) { - faro.api.pushEvent('Request failed', { url }); - faro.api.pushError(error); - span.setStatus({ code: SpanStatusCode.ERROR }); - span.end(); - reject(error); - } - }); - }); - } else { - try { - const response = await fetch(url, requestConfig); - faro?.api.pushEvent('Request completed', { url }); - return response; - } catch (error) { - faro?.api.pushEvent('Request failed', { url }); - faro?.api.pushError(error); - throw new Error(error); +const showApiError = (errorResponse: Response) => { + if (errorResponse.status >= 400 && errorResponse.status < 500) { + const text = formatBackendError(errorResponse.statusText); + if (text) { + openErrorNotification(text); } } }; -const onCallApi = createClient({ +export const getCustomFetchFn = + ({ withGlobalErrorHandler }: { withGlobalErrorHandler: boolean }) => + async (url: string, reqConfig: Parameters[1] = {}): Promise => { + const { faro } = FaroHelper; + const otel = faro?.api?.getOTEL(); + const requestConfig = { + ...reqConfig, + headers: { + ...reqConfig.headers, + 'Content-Type': 'application/json', + /** + * In short, this header will tell the Grafana plugin proxy, a Go service which use Go's HTTP Transport, + * to retry POST requests (and other non-idempotent requests). This doesn't necessarily make these requests + * idempotent, but it will make them retry-able from Go's (read: net/http) perspective. + * + * https://stackoverflow.com/questions/42847294/how-to-catch-http-server-closed-idle-connection-error/62292758#62292758 + * https://raintank-corp.slack.com/archives/C01C4K8DETW/p1692280544382739?thread_ts=1692279329.797149&cid=C01C4K8DETW + */ 'X-Idempotency-Key': `${Date.now()}-${Math.random()}`, + }, + }; + + if (faro && otel) { + const tracer = otel.trace.getTracer('default'); + let span = otel.trace.getActiveSpan(); + + if (!span) { + span = tracer.startSpan('http-request'); + span.setAttribute('page_url', document.URL.split('//')[1]); + span.setAttribute(SemanticAttributes.HTTP_URL, url); + span.setAttribute(SemanticAttributes.HTTP_METHOD, requestConfig.method); + } + + return new Promise((resolve, reject) => { + otel.context.with(otel.trace.setSpan(otel.context.active(), span), async () => { + faro.api.pushEvent('Sending request', { url }); + + const res = await fetch(url, requestConfig); + + if (res.ok) { + faro.api.pushEvent('Request completed', { url }); + span.end(); + resolve(res); + } else { + const errorData = await res.json(); + faro.api.pushEvent('Request failed', { url }); + faro.api.pushError(errorData); + span.setStatus({ code: SpanStatusCode.ERROR }); + span.end(); + if (withGlobalErrorHandler) { + showApiError(res); + } + reject(errorData); + } + }); + }); + } else { + const res = await fetch(url, requestConfig); + if (res.ok) { + faro?.api.pushEvent('Request completed', { url }); + return res; + } else { + const errorData = await res.json(); + faro?.api.pushEvent('Request failed', { url }); + faro?.api.pushError(errorData); + if (withGlobalErrorHandler) { + showApiError(res); + } + throw errorData; + } + } + }; + +const clientConfig = { baseUrl: `${API_PROXY_PREFIX}${API_PATH_PREFIX}`, querySerializer: (params: unknown) => qs.stringify(params, { arrayFormat: 'none' }), - fetch: customFetch, +}; + +// We might want to switch to middleware instead of 2 clients once this is published: https://github.com/drwpow/openapi-typescript/pull/1521 +const onCallApiWithGlobalErrorHandling = createClient({ + ...clientConfig, + fetch: getCustomFetchFn({ withGlobalErrorHandler: true }), +}); +const onCallApiSkipErrorHandling = createClient({ + ...clientConfig, + fetch: getCustomFetchFn({ withGlobalErrorHandler: false }), }); -export default onCallApi; +export const onCallApi = ({ skipErrorHandling = false }: { skipErrorHandling?: boolean } = {}) => + skipErrorHandling ? onCallApiSkipErrorHandling : onCallApiWithGlobalErrorHandling; diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index d3eead59..e37c4937 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -44,6 +44,7 @@ import { prepareForUpdate } from 'containers/AddResponders/AddResponders.helpers import { UserResponder } from 'containers/AddResponders/AddResponders.types'; import { AttachIncidentForm } from 'containers/AttachIncidentForm/AttachIncidentForm'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; import { Alert, AlertAction, TimeLineItem, TimeLineRealm, GroupedAlert } from 'models/alertgroup/alertgroup.types'; import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types'; import { User } from 'models/user/user.types'; @@ -270,17 +271,14 @@ class _IncidentPage extends React.Component; - const sourceLink = incident?.render_for_web?.source_link; return ( diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 893dcaa5..d1b5447d 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -35,6 +35,7 @@ import { IncidentsFiltersType } from 'containers/IncidentsFilters/IncidentFilter import { RemoteFilters } from 'containers/RemoteFilters/RemoteFilters'; import { TeamName } from 'containers/TeamName/TeamName'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; import { Alert, Alert as AlertType, @@ -629,7 +630,10 @@ class _IncidentsPage extends React.Component { + isSpecificIntegration: (alertReceiveChannel: ApiSchemas['AlertReceiveChannel'] | string, name: string) => { if (!alertReceiveChannel) { return false; } @@ -46,7 +47,7 @@ export const IntegrationHelper = { return slice.length === line.length ? slice : `${slice} ...`; }, - getMaintenanceText(maintenanceUntill: number, mode: number = undefined) { + getMaintenanceText(maintenanceUntill: number, mode?: MaintenanceMode) { const date = dayjs(new Date(maintenanceUntill * 1000)); const now = dayjs(); const hourDiff = date.diff(now, 'hours'); @@ -124,4 +125,5 @@ export const IntegrationHelper = { }, }; -export const getIsBidirectionalIntegration = ({ integration }: AlertReceiveChannel) => integration === 'servicenow'; +export const getIsBidirectionalIntegration = ({ integration }: ApiSchemas['AlertReceiveChannel']) => + integration === ('servicenow' as ApiSchemas['AlertReceiveChannel']['integration']); // TODO: add service now in backend schema as valid value and remove casting diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index 1d4ef6a1..363ef31d 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -54,12 +54,11 @@ import { TeamName } from 'containers/TeamName/TeamName'; import { UserDisplayWithAvatar } from 'containers/UserDisplay/UserDisplayWithAvatar'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { HeartIcon, HeartRedIcon } from 'icons/Icons'; -import { - AlertReceiveChannel, - AlertReceiveChannelCounters, -} from 'models/alert_receive_channel/alert_receive_channel.types'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; +import { AlertReceiveChannelCounters } from 'models/alert_receive_channel/alert_receive_channel.types'; import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { IntegrationHelper, getIsBidirectionalIntegration } from 'pages/integration/Integration.helper'; import styles from 'pages/integration/Integration.module.scss'; import { AppFeature } from 'state/features'; @@ -154,7 +153,7 @@ class _IntegrationPage extends React.Component @@ -583,14 +582,13 @@ class _IntegrationPage extends React.Component { - alertReceiveChannelStore - .createChannelFilter({ - alert_receive_channel: id, - filtering_term: NEW_ROUTE_DEFAULT, - filtering_term_type: 1, // non-regex - }) + AlertReceiveChannelHelper.createChannelFilter({ + alert_receive_channel: id, + filtering_term: NEW_ROUTE_DEFAULT, + filtering_term_type: 1, // non-regex + }) .then(async (channelFilter: ChannelFilter) => { - await alertReceiveChannelStore.updateChannelFilters(id); + await alertReceiveChannelStore.fetchChannelFilters(id); this.setState( (prevState) => ({ @@ -693,7 +691,7 @@ class _IntegrationPage extends React.Component { - alertReceiveChannelStore.updateChannelFilters(id, true).then(() => { + alertReceiveChannelStore.fetchChannelFilters(id, true).then(() => { escalationPolicyStore.updateEscalationPolicies(channelFilter.escalation_chain); }); this.setState({ @@ -753,13 +751,10 @@ class _IntegrationPage extends React.Component { - const { - store: { alertReceiveChannelStore }, - history, - } = this.props; - - alertReceiveChannelStore.deleteAlertReceiveChannel(id).then(() => history.push(`${PLUGIN_ROOT}/integrations/`)); + onRemovalFn = (id: ApiSchemas['AlertReceiveChannel']['id']) => { + AlertReceiveChannelHelper.deleteAlertReceiveChannel(id).then(() => + this.props.history.push(`${PLUGIN_ROOT}/integrations/`) + ); }; async loadData() { @@ -775,18 +770,18 @@ class _IntegrationPage extends React.Component this.loadExtraData(id))); + promises.push(alertReceiveChannelStore.fetchItemById(id).then(() => this.loadExtraData(id))); } else { promises.push(this.loadExtraData(id)); } if (!alertReceiveChannelStore.channelFilterIds[id]) { - promises.push(alertReceiveChannelStore.updateChannelFilters(id)); + promises.push(alertReceiveChannelStore.fetchChannelFilters(id)); } - promises.push(alertReceiveChannelStore.updateTemplates(id)); + promises.push(alertReceiveChannelStore.fetchTemplates(id)); promises.push(IntegrationHelper.fetchChatOps(store)); - promises.push(alertReceiveChannelStore.updateCountersForIntegration(id)); + promises.push(alertReceiveChannelStore.fetchCountersForIntegration(id)); await Promise.all(promises) .catch(() => { @@ -798,12 +793,12 @@ class _IntegrationPage extends React.Component this.setState({ isLoading: false })); } - async loadExtraData(id: AlertReceiveChannel['id']) { + async loadExtraData(id: ApiSchemas['AlertReceiveChannel']['id']) { const { alertReceiveChannelStore } = this.props.store; if (IntegrationHelper.isSpecificIntegration(alertReceiveChannelStore.items[id], 'grafana_alerting')) { // this will be delayed and not awaitable so that we don't delay the whole page load - return await alertReceiveChannelStore.updateConnectedContactPoints(id); + return await alertReceiveChannelStore.fetchConnectedContactPoints(id); } return Promise.resolve(); @@ -812,7 +807,7 @@ class _IntegrationPage extends React.Component void; } @@ -843,7 +838,7 @@ const IntegrationActions: React.FC = ({ const [isDemoModalOpen, setIsDemoModalOpen] = useState(false); const [maintenanceData, setMaintenanceData] = useState<{ disabled: boolean; - alert_receive_channel_id: AlertReceiveChannel['id']; + alert_receive_channel_id: ApiSchemas['AlertReceiveChannel']['id']; }>(undefined); const { id } = alertReceiveChannel; @@ -876,9 +871,11 @@ const IntegrationActions: React.FC = ({ setIsIntegrationSettingsOpen(false)} - onSubmit={() => alertReceiveChannelStore.updateItem(alertReceiveChannel['id'])} + onSubmit={async () => { + await alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id); + }} id={alertReceiveChannel['id']} - navigateToAlertGroupLabels={(_id: AlertReceiveChannel['id']) => { + navigateToAlertGroupLabels={(_id: ApiSchemas['AlertReceiveChannel']['id']) => { setIsIntegrationSettingsOpen(false); setLabelsFormOpen(true); }} @@ -890,7 +887,7 @@ const IntegrationActions: React.FC = ({ onHide={() => { setLabelsFormOpen(false); }} - onSubmit={() => alertReceiveChannelStore.updateItem(alertReceiveChannel['id'])} + onSubmit={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)} id={alertReceiveChannel['id']} onOpenIntegrationSettings={() => { setIsIntegrationSettingsOpen(true); @@ -908,7 +905,7 @@ const IntegrationActions: React.FC = ({ {maintenanceData && ( alertReceiveChannelStore.updateItem(alertReceiveChannel.id)} + onUpdate={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)} onHide={() => setMaintenanceData(undefined)} /> )} @@ -1104,16 +1101,15 @@ const IntegrationActions: React.FC = ({ } function onIntegrationMigrate() { - alertReceiveChannelStore - .migrateChannel(alertReceiveChannel.id) + AlertReceiveChannelHelper.migrateChannel(alertReceiveChannel.id) .then(() => { setConfirmModal(undefined); openNotification('Integration has been successfully migrated.'); }) .then(() => Promise.all([ - alertReceiveChannelStore.updateItem(alertReceiveChannel.id), - alertReceiveChannelStore.updateTemplates(alertReceiveChannel.id), + alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id), + alertReceiveChannelStore.fetchTemplates(alertReceiveChannel.id), ]) ) .catch(() => openErrorNotification('An error has occurred. Please try again.')); @@ -1124,8 +1120,7 @@ const IntegrationActions: React.FC = ({ } function deleteIntegration() { - alertReceiveChannelStore - .deleteAlertReceiveChannel(alertReceiveChannel.id) + AlertReceiveChannelHelper.deleteAlertReceiveChannel(alertReceiveChannel.id) .then(() => history.push(`${PLUGIN_ROOT}/integrations`)) .then(() => openNotification('Integration has been succesfully deleted.')) .catch(() => openErrorNotification('An error has occurred. Please try again.')); @@ -1146,16 +1141,16 @@ const IntegrationActions: React.FC = ({ async function onStopMaintenance() { setConfirmModal(undefined); - await alertReceiveChannelStore.stopMaintenanceMode(id); + await AlertReceiveChannelHelper.stopMaintenanceMode(id); openNotification('Maintenance has been stopped'); - await alertReceiveChannelStore.updateItem(id); + await alertReceiveChannelStore.fetchItemById(id); } }; interface IntegrationHeaderProps { alertReceiveChannelCounter: AlertReceiveChannelCounters; - alertReceiveChannel: AlertReceiveChannel; + alertReceiveChannel: ApiSchemas['AlertReceiveChannel']; integration: SelectOption; renderLabels: boolean; } @@ -1269,7 +1264,7 @@ const IntegrationHeader: React.FC = ({ ); } - function renderHeartbeat(alertReceiveChannel: AlertReceiveChannel) { + function renderHeartbeat(alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) { const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id]; const heartbeat = heartbeatStore.items[heartbeatId]; diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 06eb3805..0d1830e7 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -41,11 +41,10 @@ import { TeamName } from 'containers/TeamName/TeamName'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { HeartIcon, HeartRedIcon } from 'icons/Icons'; import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel'; -import { - AlertReceiveChannel, - MaintenanceMode, - SupportedIntegrationFilters, -} from 'models/alert_receive_channel/alert_receive_channel.types'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; +import { MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; +import { operations } from 'network/oncall-api/autogenerated-api.types'; import { IntegrationHelper } from 'pages/integration/Integration.helper'; import { AppFeature } from 'state/features'; import { PageProps, WithStoreProps } from 'state/types'; @@ -79,9 +78,9 @@ const cx = cn.bind(styles); const FILTERS_DEBOUNCE_MS = 500; interface IntegrationsState extends PageBaseState { - integrationsFilters: SupportedIntegrationFilters; - alertReceiveChannelId?: AlertReceiveChannel['id'] | 'new'; - alertReceiveChannelIdToShowLabels?: AlertReceiveChannel['id']; + integrationsFilters: operations['alert_receive_channels_list']['parameters']['query']; + alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id'] | 'new'; + alertReceiveChannelIdToShowLabels?: ApiSchemas['AlertReceiveChannel']['id']; confirmationModal: { isOpen: boolean; title: any; @@ -103,7 +102,7 @@ class _IntegrationsPage extends React.Component this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } })); } @@ -157,25 +156,25 @@ class _IntegrationsPage extends React.Component ({ ...this.state.integrationsFilters, ...(this.state.activeTab === TabType.DirectPaging - ? { integration: ['direct_paging'] } + ? { integration: ['direct_paging' as const] } : { - integration_ne: ['direct_paging'], + integration_ne: ['direct_paging' as const], integration: this.state.integrationsFilters.integration?.filter( (integration) => integration !== 'direct_paging' ), }), }); - update = () => { + update = async () => { const { store } = this.props; const page = store.filtersStore.currentTablePageNum[PAGE.Integrations]; LocationHelper.update({ p: page }, 'partial'); - return store.alertReceiveChannelStore.updatePaginatedItems({ + await store.alertReceiveChannelStore.fetchPaginatedItems({ filters: this.getFiltersBasedOnCurrentTab(), page, - updateCounters: false, + shouldFetchCounters: false, invalidateFn: () => this.invalidateRequestFn(page), }); }; @@ -202,8 +201,7 @@ class _IntegrationsPage extends React.Component { + navigateToAlertGroupLabels={(id: ApiSchemas['AlertReceiveChannel']['id']) => { this.setState({ alertReceiveChannelId: undefined, alertReceiveChannelIdToShowLabels: id }); }} /> @@ -306,7 +304,7 @@ class _IntegrationsPage extends React.Component { + onOpenIntegrationSettings={(id: ApiSchemas['AlertReceiveChannel']['id']) => { this.setState({ alertReceiveChannelId: id }); }} /> @@ -333,7 +331,7 @@ class _IntegrationsPage extends React.Component { + renderName = (item: ApiSchemas['AlertReceiveChannel']) => { const { query } = this.props; return ( @@ -353,9 +351,9 @@ class _IntegrationsPage extends React.Component @@ -482,7 +480,7 @@ class _IntegrationsPage extends React.Component { + renderButtons = (item: ApiSchemas['AlertReceiveChannel']) => { const { store } = this.props; return ( @@ -573,13 +571,14 @@ class _IntegrationsPage extends React.Component this.renderIntegrationStatus(item, alertReceiveChannelStore), + render: (item: ApiSchemas['AlertReceiveChannel']) => + this.renderIntegrationStatus(item, alertReceiveChannelStore), }, { width: '25%', title: 'Type', key: 'datasource', - render: (item: AlertReceiveChannel) => this.renderDatasource(item, alertReceiveChannelStore), + render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderDatasource(item, alertReceiveChannelStore), }, ...(isMonitoringSystemsTab ? [ @@ -587,25 +586,25 @@ class _IntegrationsPage extends React.Component this.renderMaintenance(item), + render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderMaintenance(item), }, { width: '5%', title: 'Heartbeat', key: 'heartbeat', - render: (item: AlertReceiveChannel) => this.renderHeartbeat(item), + render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderHeartbeat(item), }, ] : []), { width: isMonitoringSystemsTab ? '15%' : '30%', title: 'Team', - render: (item: AlertReceiveChannel) => this.renderTeam(item, grafanaTeamStore.items), + render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderTeam(item, grafanaTeamStore.items), }, { width: '50px', key: 'buttons', - render: (item: AlertReceiveChannel) => this.renderButtons(item), + render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderButtons(item), className: cx('buttons'), }, ]; @@ -614,7 +613,7 @@ class _IntegrationsPage extends React.Component ( + render: ({ labels }: ApiSchemas['AlertReceiveChannel']) => ( applyLabelFilter(label, PAGE.Integrations)} /> ), }); @@ -636,20 +635,16 @@ class _IntegrationsPage extends React.Component { + onIntegrationEditClick = (id: ApiSchemas['AlertReceiveChannel']['id']) => { this.setState({ alertReceiveChannelId: id }); }; - onLabelsEditClick = (id: AlertReceiveChannel['id']) => { + onLabelsEditClick = (id: ApiSchemas['AlertReceiveChannel']['id']) => { this.setState({ alertReceiveChannelIdToShowLabels: id }); }; - handleDeleteAlertReceiveChannel = (alertReceiveChannelId: AlertReceiveChannel['id']) => { - const { store } = this.props; - - const { alertReceiveChannelStore } = store; - - alertReceiveChannelStore.deleteAlertReceiveChannel(alertReceiveChannelId).then(this.applyFilters); + handleDeleteAlertReceiveChannel = (alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']) => { + AlertReceiveChannelHelper.deleteAlertReceiveChannel(alertReceiveChannelId).then(this.applyFilters); this.setState({ confirmationModal: undefined }); }; @@ -666,10 +661,10 @@ class _IntegrationsPage extends React.Component this.invalidateRequestFn(newPage), }) .then(() => { diff --git a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx index f5416aa6..767a7de3 100644 --- a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx +++ b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx @@ -106,6 +106,7 @@ class _SlackSettings extends Component { const { organizationStore: { currentOrganization }, slackStore, + slackChannelStore, } = store; return ( @@ -119,9 +120,12 @@ class _SlackSettings extends Component { tooltip="The selected channel will be used as a fallback in the event that a schedule or integration does not have a configured channel" > - showSearch - modelName="slackChannelStore" + items={slackChannelStore.items} + fetchItemsFn={slackChannelStore.updateItems} + fetchItemFn={slackChannelStore.updateItem} + getSearchResult={slackChannelStore.getSearchResult} displayField="display_name" valueField="id" placeholder="Select Slack Channel" @@ -201,17 +205,22 @@ class _SlackSettings extends Component { }; renderSlackChannels = () => { - const { store } = this.props; + const { + store: { organizationStore, slackChannelStore }, + } = this.props; return ( - showSearch className={cx('select', 'control')} - modelName="slackChannelStore" + items={slackChannelStore.items} + fetchItemsFn={slackChannelStore.updateItems} + fetchItemFn={slackChannelStore.updateItem} + getSearchResult={slackChannelStore.getSearchResult} displayField="display_name" valueField="id" placeholder="Select Slack Channel" - value={store.organizationStore.currentOrganization?.slack_channel?.id} + value={organizationStore.currentOrganization?.slack_channel?.id} onChange={this.handleSlackChannelChange} nullItemName={PRIVATE_CHANNEL_NAME} /> diff --git a/grafana-plugin/src/state/rootBaseStore/RootBaseStore.test.ts b/grafana-plugin/src/state/rootBaseStore/RootBaseStore.test.ts index 8618b8ba..29c555d7 100644 --- a/grafana-plugin/src/state/rootBaseStore/RootBaseStore.test.ts +++ b/grafana-plugin/src/state/rootBaseStore/RootBaseStore.test.ts @@ -181,7 +181,7 @@ describe('rootBaseStore', () => { }); isUserActionAllowed.mockReturnValueOnce(true); PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null); - rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser; + Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser }); // test await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl)); @@ -224,7 +224,7 @@ describe('rootBaseStore', () => { }); isUserActionAllowed.mockReturnValueOnce(true); PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null); - rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser; + Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser }); // test await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl)); @@ -300,7 +300,7 @@ describe('rootBaseStore', () => { version: 'asdfasdf', license: 'asdfasdf', }); - rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser; + Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser }); // test await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl)); @@ -329,7 +329,7 @@ describe('rootBaseStore', () => { license: 'asdfasdf', }); PluginState.updatePluginStatus = jest.fn().mockResolvedValueOnce(updatePluginStatusError); - rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser; + Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser }); // test await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl)); diff --git a/grafana-plugin/src/state/rootBaseStore/RootBaseStore.ts b/grafana-plugin/src/state/rootBaseStore/RootBaseStore.ts index 3a63ca0b..1677355a 100644 --- a/grafana-plugin/src/state/rootBaseStore/RootBaseStore.ts +++ b/grafana-plugin/src/state/rootBaseStore/RootBaseStore.ts @@ -5,7 +5,6 @@ import qs from 'query-string'; import { OnCallAppPluginMeta } from 'types'; import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel'; -import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { AlertReceiveChannelFiltersStore } from 'models/alert_receive_channel_filters/alert_receive_channel_filters'; import { AlertGroupStore } from 'models/alertgroup/alertgroup'; import { ApiTokenStore } from 'models/api_token/api_token'; @@ -31,6 +30,7 @@ import { TimezoneStore } from 'models/timezone/timezone'; import { UserStore } from 'models/user/user'; import { UserGroupStore } from 'models/user_group/user_group'; import { makeRequest } from 'network/network'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { AppFeature } from 'state/features'; import { PluginState } from 'state/plugin/plugin'; import { retryFailingPromises } from 'utils/async'; @@ -71,7 +71,7 @@ export class RootBaseStore { initialQuery = qs.parse(window.location.search); @observable - selectedAlertReceiveChannel?: AlertReceiveChannel['id']; + selectedAlertReceiveChannel?: ApiSchemas['AlertReceiveChannel']['id']; @observable features?: { [key: string]: boolean }; @@ -141,7 +141,7 @@ export class RootBaseStore { Promise.all([ this.userStore.updateNotificationPolicyOptions(), this.userStore.updateNotifyByOptions(), - this.alertReceiveChannelStore.updateAlertReceiveChannelOptions(), + this.alertReceiveChannelStore.fetchAlertReceiveChannelOptions(), ]); }; diff --git a/grafana-plugin/src/utils/types.ts b/grafana-plugin/src/utils/types.ts index ed63e75f..2de8df54 100644 --- a/grafana-plugin/src/utils/types.ts +++ b/grafana-plugin/src/utils/types.ts @@ -12,3 +12,18 @@ export interface TableColumn { export type PropertiesThatExtendsAnotherClass = keyof { [Prop in keyof OriginalObj as OriginalObj[Prop] extends AnotherClass ? Prop : never]: unknown; }; + +// IfEquals, WritableKeys, ReadonlyKeys based on https://stackoverflow.com/a/49579497/4931398 +export type IfEquals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 + ? A + : B; + +export type WritableKeys = { + [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>; +}[keyof T]; + +export type ReadonlyKeys = { + [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, never, P>; +}[keyof T]; + +export type OmitReadonlyMembers = Omit>; diff --git a/grafana-plugin/src/utils/utils.ts b/grafana-plugin/src/utils/utils.ts index b105f128..49aadcae 100644 --- a/grafana-plugin/src/utils/utils.ts +++ b/grafana-plugin/src/utils/utils.ts @@ -18,18 +18,18 @@ export class KeyValuePair { } } +export const formatBackendError = (payload: string | Record) => + typeof payload === 'string' + ? payload + : Object.keys(payload) + .map((key) => `${sentenceCase(key)}: ${payload[key]}`) + .join('\n'); + export function showApiError(error: any) { if (isNetworkError(error) && error.response && error.response.status >= 400 && error.response.status < 500) { - const payload = error.response.data; - const text = - typeof payload === 'string' - ? payload - : Object.keys(payload) - .map((key) => `${sentenceCase(key)}: ${payload[key]}`) - .join('\n'); + const text = formatBackendError(error.response.data); openErrorNotification(text); } - throw error; }