This commit is contained in:
Joey Orlando 2024-01-04 17:16:08 -05:00 committed by GitHub
commit e97ec13e38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 1121 additions and 825 deletions

View file

@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
## v1.3.82 (2024-01-04)
### Added
- Add ability to create an Outgoing Webhook with the PATCH HTTP method via the UI by @joeyorlando ([#3604](https://github.com/grafana/oncall/pull/3604))
### Changed
- Handle message to reply to not found in Telegram send log ([#3587](https://github.com/grafana/oncall/pull/3587))
- Upgrade mobx lib to the latest version 6.12.0 ([#3453](https://github.com/grafana/oncall/issues/3453))
- Add task lock to avoid running multiple sync_organization tasks in parallel for the same org ([#3612](https://github.com/grafana/oncall/pull/3612))
## v1.3.81 (2023-12-28)
### Added

View file

@ -8,6 +8,7 @@ from apps.api.views.features import (
FEATURE_GRAFANA_CLOUD_CONNECTION,
FEATURE_GRAFANA_CLOUD_NOTIFICATIONS,
FEATURE_LIVE_SETTINGS,
FEATURE_MSTEAMS,
FEATURE_SLACK,
FEATURE_TELEGRAM,
)
@ -77,6 +78,22 @@ def test_oss_features_enabled_in_oss_installation_by_default(
assert response.status_code == status.HTTP_200_OK
assert FEATURE_GRAFANA_CLOUD_CONNECTION in response.json()
assert FEATURE_GRAFANA_CLOUD_NOTIFICATIONS in response.json()
assert FEATURE_MSTEAMS not in response.json()
@pytest.mark.django_db
@override_settings(IS_OPEN_SOURCE=False)
def test_non_oss_features_enabled(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
):
_, user, token = make_organization_and_user_with_plugin_token()
client = APIClient()
url = reverse("api-internal:features")
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
assert FEATURE_MSTEAMS in response.json()
@pytest.mark.django_db

View file

@ -8,6 +8,7 @@ from apps.auth_token.auth import PluginAuthentication
from apps.base.utils import live_settings
from apps.labels.utils import is_labels_feature_enabled
FEATURE_MSTEAMS = "msteams"
FEATURE_SLACK = "slack"
FEATURE_TELEGRAM = "telegram"
FEATURE_LIVE_SETTINGS = "live_settings"
@ -55,6 +56,8 @@ class FeaturesAPIView(APIView):
enabled_features.append(FEATURE_LIVE_SETTINGS)
if live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED:
enabled_features.append(FEATURE_GRAFANA_CLOUD_NOTIFICATIONS)
else:
enabled_features.append(FEATURE_MSTEAMS)
if settings.FEATURE_GRAFANA_ALERTING_V2_ENABLED:
enabled_features.append(FEATURE_GRAFANA_ALERTING_V2)

View file

@ -170,6 +170,12 @@ def send_log_and_actions_message(self, channel_chat_id, group_chat_id, channel_m
f"due to 'Chat not found'. alert_group {alert_group.pk}"
)
return
elif e.message == "Message to reply not found":
logger.warning(
f"Could not send log and actions messages to Telegram group with id {group_chat_id} "
f"due to 'Message to reply not found'. alert_group {alert_group.pk}"
)
return
else:
raise

View file

@ -0,0 +1,47 @@
from unittest.mock import patch
import pytest
from telegram import error
from apps.telegram.client import TelegramClient
from apps.telegram.models import TelegramMessage
from apps.telegram.tasks import send_log_and_actions_message
@patch.object(TelegramClient, "send_raw_message", side_effect=error.BadRequest("Message to reply not found"))
@pytest.mark.django_db
def test_send_log_and_actions_replied_message_not_found(
mock_send_message,
make_organization_and_user,
make_telegram_user_connector,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_telegram_message,
caplog,
):
# set up a user with Telegram account connected
organization, user = make_organization_and_user()
make_telegram_user_connector(user)
# create an alert group with an existing Telegram message in user's DM
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload)
telegram_message = make_telegram_message(
alert_group=alert_group,
message_type=TelegramMessage.PERSONAL_MESSAGE,
chat_id=str(user.telegram_connection.telegram_chat_id),
message_id=123,
)
reply_to_message_id = 321
send_log_and_actions_message(
telegram_message.chat_id, "group_chat_id", telegram_message.message_id, reply_to_message_id
)
expected_msg = (
f"Could not send log and actions messages to Telegram group with id group_chat_id "
f"due to 'Message to reply not found'. alert_group {alert_group.pk}"
)
assert expected_msg in caplog.text

View file

@ -1,4 +1,5 @@
import logging
import uuid
from celery.utils.log import get_task_logger
from django.conf import settings
@ -7,12 +8,25 @@ from django.utils import timezone
from apps.grafana_plugin.helpers.client import GcomAPIClient, GrafanaAPIClient
from apps.user_management.models import Organization, Team, User
from apps.user_management.signals import org_sync_signal
from common.utils import task_lock
logger = get_task_logger(__name__)
logger.setLevel(logging.DEBUG)
def sync_organization(organization: Organization) -> None:
# ensure one sync task is running at most for a given org at a given time
lock_id = "sync-organization-lock-{}".format(organization.id)
random_value = str(uuid.uuid4())
with task_lock(lock_id, random_value) as acquired:
if acquired:
_sync_organization(organization)
else:
# sync already running
logger.info(f"Sync for Organization {organization.pk} already in progress.")
def _sync_organization(organization: Organization) -> None:
grafana_api_client = GrafanaAPIClient(api_url=organization.grafana_url, api_token=organization.api_token)
# NOTE: checking whether or not RBAC is enabled depends on whether we are dealing with an open-source or cloud

View file

@ -326,10 +326,13 @@ def test_sync_organization_is_rbac_permissions_enabled_open_source(make_organiza
@pytest.mark.parametrize("gcom_api_response", [False, True])
@patch("apps.user_management.sync.GcomAPIClient")
@patch("common.utils.cache")
@override_settings(LICENSE=settings.CLOUD_LICENSE_NAME)
@override_settings(GRAFANA_COM_ADMIN_API_TOKEN="mockedToken")
@pytest.mark.django_db
def test_sync_organization_is_rbac_permissions_enabled_cloud(mocked_gcom_client, make_organization, gcom_api_response):
def test_sync_organization_is_rbac_permissions_enabled_cloud(
mock_cache, mocked_gcom_client, make_organization, gcom_api_response
):
stack_id = 5
organization = make_organization(stack_id=stack_id)
@ -369,22 +372,27 @@ def test_sync_organization_is_rbac_permissions_enabled_cloud(mocked_gcom_client,
},
)
with patch.object(GrafanaAPIClient, "check_token", return_value=(None, api_check_token_call_status)):
with patch.object(GrafanaAPIClient, "get_users", return_value=api_users_response):
with patch.object(GrafanaAPIClient, "get_teams", return_value=(api_teams_response, None)):
with patch.object(GrafanaAPIClient, "get_team_members", return_value=(api_members_response, None)):
with patch.object(
GrafanaAPIClient,
"get_grafana_incident_plugin_settings",
return_value=(
{"enabled": True, "jsonData": {"backendUrl": MOCK_GRAFANA_INCIDENT_BACKEND_URL}},
None,
),
):
sync_organization(organization)
random_uuid = "random"
with patch("apps.user_management.sync.uuid.uuid4", return_value=random_uuid):
with patch.object(GrafanaAPIClient, "check_token", return_value=(None, api_check_token_call_status)):
with patch.object(GrafanaAPIClient, "get_users", return_value=api_users_response):
with patch.object(GrafanaAPIClient, "get_teams", return_value=(api_teams_response, None)):
with patch.object(GrafanaAPIClient, "get_team_members", return_value=(api_members_response, None)):
with patch.object(
GrafanaAPIClient,
"get_grafana_incident_plugin_settings",
return_value=(
{"enabled": True, "jsonData": {"backendUrl": MOCK_GRAFANA_INCIDENT_BACKEND_URL}},
None,
),
):
sync_organization(organization)
organization.refresh_from_db()
# lock is set and released
mock_cache.add.assert_called_once_with(f"sync-organization-lock-{organization.id}", random_uuid, 60 * 10)
mock_cache.delete.assert_called_once_with(f"sync-organization-lock-{organization.id}")
assert mocked_gcom_client.return_value.called_once_with("mockedToken")
assert mocked_gcom_client.return_value.is_rbac_enabled_for_stack.called_once_with(stack_id)
assert organization.is_rbac_permissions_enabled == gcom_api_response
@ -433,3 +441,19 @@ def test_cleanup_organization_deleted(make_organization):
organization.refresh_from_db()
assert organization.deleted_at is not None
@pytest.mark.django_db
def test_sync_organization_lock(make_organization):
organization = make_organization()
random_uuid = "random"
with patch("apps.user_management.sync.GrafanaAPIClient") as mock_client:
with patch("apps.user_management.sync.uuid.uuid4", return_value=random_uuid):
with patch("apps.user_management.sync.task_lock") as mock_task_lock:
# lock couldn't be acquired
mock_task_lock.return_value.__enter__.return_value = False
sync_organization(organization)
mock_task_lock.assert_called_once_with(f"sync-organization-lock-{organization.id}", random_uuid)
assert not mock_client.called

View file

@ -5,6 +5,7 @@ import os
import random
import re
import time
from contextlib import contextmanager
from functools import reduce
import factory
@ -12,6 +13,7 @@ import markdown2
from bs4 import BeautifulSoup
from celery.utils.log import get_task_logger
from celery.utils.time import get_exponential_backoff_interval
from django.core.cache import cache
from django.utils.html import urlize
logger = get_task_logger(__name__)
@ -73,6 +75,30 @@ class OkToRetry:
)
LOCK_EXPIRE = 60 * 10 # Lock expires in 10 minutes
# Context manager for tasks that are intended to run once at a time
# (ie. no parallel instances of the same task running)
# based on https://docs.celeryq.dev/en/stable/tutorials/task-cookbook.html#ensuring-a-task-is-only-executed-one-at-a-time
@contextmanager
def task_lock(lock_id, oid):
timeout_at = time.monotonic() + LOCK_EXPIRE - 3
# cache.add returns False if the key already exists
status = cache.add(lock_id, oid, LOCK_EXPIRE)
try:
yield status
finally:
# cache delete may be slow, but we have to use it to take
# advantage of using add() for atomic locking
if time.monotonic() < timeout_at and status:
# don't release the lock if we exceeded the timeout
# to lessen the chance of releasing an expired lock
# owned by someone else
# also don't release the lock if we didn't acquire it
cache.delete(lock_id)
# lru cache version with addition of timeout.
# Timeout added to not to occupy memory with too old values
def timed_lru_cache(timeout: int, maxsize: int = 128, typed: bool = False):

View file

@ -5,10 +5,7 @@ whitenoise==5.3.0
twilio~=6.37.0
phonenumbers==8.10.0
celery[amqp,redis]==5.3.1
# NOTE: temporarily installing a forked version of redis-py which adds some more debug logging
# in an effort to fix https://github.com/grafana/oncall-private/issues/2406
# revert this change once done debugging
git+https://github.com/grafana/redis-py@c0f167c
redis==5.0.1
humanize==0.5.1
uwsgi==2.0.21
django-cors-headers==3.7.0

View file

@ -8,7 +8,7 @@
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-transform-destructuring", { "useBuiltIns": true }],
"@babel/plugin-transform-runtime",
"@babel/proposal-class-properties",
["@babel/plugin-proposal-class-properties", { "loose": false }],
"@babel/transform-regenerator",
"@babel/plugin-transform-template-literals"
]

View file

@ -133,8 +133,8 @@
"dayjs": "^1.11.5",
"eslint-plugin-import": "^2.25.4",
"immutability-helper": "^3.1.1",
"mobx": "5.13.0",
"mobx-react": "6.1.1",
"mobx": "6.12.0",
"mobx-react": "9.1.0",
"object-hash": "^3.0.0",
"openapi-fetch": "^0.8.1",
"prettier": "^2.8.2",

View file

@ -23,7 +23,6 @@ import {
import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook';
import { ScheduleStore } from 'models/schedule/schedule';
import { WaitDelay } from 'models/wait_delay';
import { SelectOption } from 'state/types';
import { getVar } from 'utils/DOM';
import { UserActions } from 'utils/authorization';
@ -255,7 +254,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
// @ts-ignore
value={wait_delay}
onChange={this._getOnSelectChangeHandler('wait_delay')}
options={waitDelays.map((waitDelay: WaitDelay) => ({
options={waitDelays.map((waitDelay: SelectOption) => ({
value: waitDelay.value,
label: waitDelay.display_name,
}))}

View file

@ -8,13 +8,12 @@ import { SortableElement } from 'react-sortable-hoc';
import PluginLink from 'components/PluginLink/PluginLink';
import Timeline from 'components/Timeline/Timeline';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { Channel } from 'models/channel';
import { NotificationPolicyType, prepareNotificationPolicy } from 'models/notification_policy';
import { NotifyBy } from 'models/notify_by';
import { Channel } from 'models/channel/channel';
import { NotificationPolicyType, prepareNotificationPolicy } from 'models/notification_policy/notification_policy';
import { User } from 'models/user/user.types';
import { WaitDelay } from 'models/wait_delay';
import { RootStore } from 'state';
import { AppFeature } from 'state/features';
import { SelectOption } from 'state/types';
import { UserAction } from 'utils/authorization';
import DragHandle from './DragHandle';
@ -34,8 +33,8 @@ export interface NotificationPolicyProps {
onDelete: (id: string) => void;
notificationChoices: any[];
channels?: any[];
waitDelays?: WaitDelay[];
notifyByOptions?: NotifyBy[];
waitDelays?: SelectOption[];
notifyByOptions?: SelectOption[];
telegramVerified: boolean;
phoneStatus: number;
isMobileAppConnected: boolean;
@ -185,7 +184,7 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
value={wait_delay}
disabled={disabled}
onChange={this._getOnChangeHandler('wait_delay')}
options={waitDelays.map((waitDelay: WaitDelay) => ({
options={waitDelays.map((waitDelay: SelectOption) => ({
label: waitDelay.display_name,
value: waitDelay.value,
}))}
@ -208,7 +207,7 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
value={notify_by}
disabled={disabled}
onChange={this._getOnChangeHandler('notify_by')}
options={notifyByOptions.map((notifyByOption: NotifyBy) => ({
options={notifyByOptions.map((notifyByOption: SelectOption) => ({
label: notifyByOption.display_name,
value: notifyByOption.value,
}))}

View file

@ -5,7 +5,7 @@ import { VerticalGroup } from '@grafana/ui';
import Timeline from 'components/Timeline/Timeline';
import SlackConnector from 'containers/AlertRules/parts/connectors/SlackConnector';
import TelegramConnector from 'containers/AlertRules/parts/connectors/TelegramConnector';
import { ChannelFilter } from 'models/channel_filter';
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';
import { getVar } from 'utils/DOM';

View file

@ -11,7 +11,7 @@ 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';
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import CommonIntegrationHelper from 'pages/integration/CommonIntegration.helper';
import IntegrationHelper from 'pages/integration/Integration.helper';
import { useStore } from 'state/useStore';

View file

@ -30,7 +30,7 @@ import styles from 'containers/IntegrationContainers/ExpandedIntegrationRouteDis
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';
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 CommonIntegrationHelper from 'pages/integration/CommonIntegration.helper';

View file

@ -10,7 +10,7 @@ import { MONACO_READONLY_CONFIG } from 'components/MonacoEditor/MonacoEditor.con
import Text from 'components/Text/Text';
import { templatesToRender } from 'containers/IntegrationContainers/IntegrationTemplatesList.config';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { AlertTemplatesDTO } from 'models/alert_templates';
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
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';

View file

@ -19,7 +19,7 @@ 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';
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 { TemplateOptions } from 'pages/integration/Integration.config';

View file

@ -122,6 +122,10 @@ export function createForm(
value: 'PUT',
label: 'PUT',
},
{
value: 'PATCH',
label: 'PATCH',
},
{
value: 'DELETE',
label: 'DELETE',

View file

@ -10,7 +10,7 @@ import SortableList from 'components/SortableList/SortableList';
import Text from 'components/Text/Text';
import Timeline from 'components/Timeline/Timeline';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { NotificationPolicyType } from 'models/notification_policy';
import { NotificationPolicyType } from 'models/notification_policy/notification_policy';
import { User as UserType } from 'models/user/user.types';
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';

View file

@ -9,7 +9,7 @@ import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.con
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';
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 { useStore } from 'state/useStore';

View file

@ -1,7 +1,7 @@
import { omit } from 'lodash-es';
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import { AlertTemplatesDTO } from 'models/alert_templates';
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';
@ -9,7 +9,6 @@ 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';
import { Mixpanel } from 'services/mixpanel';
import { RootStore } from 'state';
import { move } from 'state/helpers';
import { SelectOption } from 'state/types';
@ -64,6 +63,8 @@ export class AlertReceiveChannelStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/alert_receive_channels/';
}
@ -95,11 +96,13 @@ export class AlertReceiveChannelStore extends BaseStore {
async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling = false): Promise<AlertReceiveChannel> {
const alertReceiveChannel = await this.getById(id, skipErrorHandling);
// @ts-ignore
this.items = {
...this.items,
[id]: omit(alertReceiveChannel, 'heartbeat'),
};
runInAction(() => {
// @ts-ignore
this.items = {
...this.items,
[id]: omit(alertReceiveChannel, 'heartbeat'),
};
});
this.populateHearbeats([alertReceiveChannel]);
@ -112,20 +115,24 @@ export class AlertReceiveChannelStore extends BaseStore {
const { results } = await makeRequest(this.path, { params });
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({
...acc,
[item.id]: omit(item, 'heartbeat'),
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({
...acc,
[item.id]: omit(item, 'heartbeat'),
}),
{}
),
};
});
this.populateHearbeats(results);
this.searchResult = results.map((item: AlertReceiveChannel) => item.id);
runInAction(() => {
this.searchResult = results.map((item: AlertReceiveChannel) => item.id);
});
this.updateCounters();
@ -149,24 +156,28 @@ export class AlertReceiveChannelStore extends BaseStore {
return undefined;
}
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({
...acc,
[item.id]: omit(item, 'heartbeat'),
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({
...acc,
[item.id]: omit(item, 'heartbeat'),
}),
{}
),
};
});
this.populateHearbeats(results);
this.paginatedSearchResult = {
count,
results: results.map((item: AlertReceiveChannel) => item.id),
page_size,
};
runInAction(() => {
this.paginatedSearchResult = {
count,
results: results.map((item: AlertReceiveChannel) => item.id),
page_size,
};
});
if (updateCounters) {
this.updateCounters();
@ -184,10 +195,12 @@ export class AlertReceiveChannelStore extends BaseStore {
return acc;
}, {});
this.rootStore.heartbeatStore.items = {
...this.rootStore.heartbeatStore.items,
...heartbeats,
};
runInAction(() => {
this.rootStore.heartbeatStore.items = {
...this.rootStore.heartbeatStore.items,
...heartbeats,
};
});
const alertReceiveChannelToHeartbeat = alertReceiveChannels.reduce(
(acc: any, alertReceiveChannel: AlertReceiveChannel) => {
@ -200,10 +213,12 @@ export class AlertReceiveChannelStore extends BaseStore {
{}
);
this.alertReceiveChannelToHeartbeat = {
...this.alertReceiveChannelToHeartbeat,
...alertReceiveChannelToHeartbeat,
};
runInAction(() => {
this.alertReceiveChannelToHeartbeat = {
...this.alertReceiveChannelToHeartbeat,
...alertReceiveChannelToHeartbeat,
};
});
}
@action
@ -220,42 +235,48 @@ export class AlertReceiveChannelStore extends BaseStore {
{}
);
this.channelFilters = {
...this.channelFilters,
...channelFilters,
};
if (isOverwrite) {
// This is needed because on Move Up/Down/Removal the store no longer reflects the correct state
runInAction(() => {
this.channelFilters = {
...this.channelFilters,
...channelFilters,
};
});
if (isOverwrite) {
runInAction(() => {
// This is needed because on Move Up/Down/Removal the store no longer reflects the correct state
this.channelFilters = {
...channelFilters,
};
});
}
this.channelFilterIds = {
...this.channelFilterIds,
[alertReceiveChannelId]: response.map((channelFilter: ChannelFilter) => channelFilter.id),
};
runInAction(() => {
this.channelFilterIds = {
...this.channelFilterIds,
[alertReceiveChannelId]: response.map((channelFilter: ChannelFilter) => channelFilter.id),
};
});
}
@action
async updateChannelFilter(channelFilterId: ChannelFilter['id']) {
const response = await makeRequest(`/channel_filters/${channelFilterId}/`, {});
this.channelFilters = {
...this.channelFilters,
[channelFilterId]: response,
};
runInAction(() => {
this.channelFilters = {
...this.channelFilters,
[channelFilterId]: response,
};
});
}
@action
async migrateChannel(id: AlertReceiveChannel['id']) {
return await makeRequest(`/alert_receive_channels/${id}/migrate`, {
method: 'POST',
});
}
@action
async createChannelFilter(data: Partial<ChannelFilter>) {
return await makeRequest('/channel_filters/', {
method: 'POST',
@ -270,10 +291,12 @@ export class AlertReceiveChannelStore extends BaseStore {
data,
});
this.channelFilters = {
...this.channelFilters,
[response.id]: response,
};
runInAction(() => {
this.channelFilters = {
...this.channelFilters,
[response.id]: response,
};
});
return response;
}
@ -284,8 +307,6 @@ export class AlertReceiveChannelStore extends BaseStore {
oldIndex: number,
newIndex: number
) {
Mixpanel.track('Move ChannelFilter', null);
const channelFilterId = this.channelFilterIds[alertReceiveChannelId][oldIndex];
this.channelFilterIds[alertReceiveChannelId] = move(
@ -301,8 +322,6 @@ export class AlertReceiveChannelStore extends BaseStore {
@action
async deleteChannelFilter(channelFilterId: ChannelFilter['id']) {
Mixpanel.track('Delete ChannelFilter', null);
const channelFilter = this.channelFilters[channelFilterId];
this.channelFilterIds[channelFilter.alert_receive_channel].splice(
@ -320,7 +339,10 @@ export class AlertReceiveChannelStore extends BaseStore {
@action.bound
async updateAlertReceiveChannelOptions() {
const response = await makeRequest(`/alert_receive_channels/integration_options/`, {});
this.alertReceiveChannelOptions = response;
runInAction(() => {
this.alertReceiveChannelOptions = response;
});
}
getIntegration(alertReceiveChannel: Partial<AlertReceiveChannel>): SelectOption {
@ -338,13 +360,14 @@ export class AlertReceiveChannelStore extends BaseStore {
async saveAlertReceiveChannel(id: AlertReceiveChannel['id'], data: Partial<AlertReceiveChannel>) {
const item = await this.update(id, data, undefined, true);
this.items = {
...this.items,
[id]: item,
};
runInAction(() => {
this.items = {
...this.items,
[id]: item,
};
});
}
@action
async deleteAlertReceiveChannel(id: AlertReceiveChannel['id']) {
return await this.delete(id);
}
@ -356,20 +379,24 @@ export class AlertReceiveChannelStore extends BaseStore {
withCredentials: true,
});
this.templates = {
...this.templates,
[alertReceiveChannelId]: response,
};
runInAction(() => {
this.templates = {
...this.templates,
[alertReceiveChannelId]: response,
};
});
}
@action
async updateItem(id: AlertReceiveChannel['id']) {
const item = await this.getById(id);
this.items = {
...this.items,
[id]: item,
};
runInAction(() => {
this.items = {
...this.items,
[id]: item,
};
});
}
@action
@ -380,10 +407,12 @@ export class AlertReceiveChannelStore extends BaseStore {
withCredentials: true,
});
this.templates = {
...this.templates,
[alertReceiveChannelId]: response,
};
runInAction(() => {
this.templates = {
...this.templates,
[alertReceiveChannelId]: response,
};
});
}
async getGrafanaAlertingContactPoints() {
@ -394,22 +423,24 @@ export class AlertReceiveChannelStore extends BaseStore {
async updateConnectedContactPoints(alertReceiveChannelId: AlertReceiveChannel['id']) {
const response = await makeRequest(`${this.path}${alertReceiveChannelId}/connected_contact_points `, {});
this.connectedContactPoints = {
...this.connectedContactPoints,
runInAction(() => {
this.connectedContactPoints = {
...this.connectedContactPoints,
[alertReceiveChannelId]: response.reduce((list: ContactPoint[], payload) => {
payload.contact_points.forEach((contactPoint: { name: string; notification_connected: boolean }) => {
list.push({
dataSourceName: payload.name,
dataSourceId: payload.uid,
contactPoint: contactPoint.name,
notificationConnected: contactPoint.notification_connected,
} as ContactPoint);
});
[alertReceiveChannelId]: response.reduce((list: ContactPoint[], payload) => {
payload.contact_points.forEach((contactPoint: { name: string; notification_connected: boolean }) => {
list.push({
dataSourceName: payload.name,
dataSourceId: payload.uid,
contactPoint: contactPoint.name,
notificationConnected: contactPoint.notification_connected,
} as ContactPoint);
});
return list;
}, []),
};
return list;
}, []),
};
});
}
async connectContactPoint(
@ -460,13 +491,6 @@ export class AlertReceiveChannelStore extends BaseStore {
return integration_log;
}
async installSentry(sentry_payload: string) {
return await makeRequest('/sentry_complete_install/', {
method: 'POST',
params: { sentry_payload },
});
}
async sendDemoAlert(id: AlertReceiveChannel['id'], payload: string = undefined) {
const requestConfig: any = {
method: 'POST',
@ -479,8 +503,6 @@ export class AlertReceiveChannelStore extends BaseStore {
}
await makeRequest(`${this.path}${id}/send_demo_alert/`, requestConfig).catch(showApiError);
Mixpanel.track('Send Demo Incident', null);
}
async sendDemoAlertToParticularRoute(id: ChannelFilter['id']) {
@ -508,25 +530,31 @@ export class AlertReceiveChannelStore extends BaseStore {
});
}
@action
async updateCounters() {
const counters = await makeRequest(`${this.path}counters`, {
method: 'GET',
});
this.counters = counters;
runInAction(() => {
this.counters = counters;
});
}
@action
async updateCountersForIntegration(id: AlertReceiveChannel['id']): Promise<any> {
const counters = await makeRequest(`${this.path}${id}/counters`, {
method: 'GET',
});
this.counters = {
...this.counters,
[id]: {
...counters[id],
},
};
runInAction(() => {
this.counters = {
...this.counters,
[id]: {
...counters[id],
},
};
});
return counters;
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
@ -15,6 +15,8 @@ export class AlertReceiveChannelFiltersStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/alert_receive_channels/';
}
@ -32,17 +34,19 @@ export class AlertReceiveChannelFiltersStore extends BaseStore {
params: { search: query, filters: true },
});
this.items = {
...this.items,
...results.reduce(
(acc: { [key: string]: SelectOption }, item: SelectOption) => ({
...acc,
[item.value]: item,
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: string]: SelectOption }, item: SelectOption) => ({
...acc,
[item.value]: item,
}),
{}
),
};
this.searchResult = results.map((item: SelectOption) => item.value);
this.searchResult = results.map((item: SelectOption) => item.value);
});
}
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import qs from 'query-string';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
@ -7,7 +7,6 @@ import { ActionKey } from 'models/loader/action-keys';
import { User } from 'models/user/user.types';
import { makeRequest } from 'network';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { Mixpanel } from 'services/mixpanel';
import { RootStore } from 'state';
import { SelectOption } from 'state/types';
import { openErrorNotification, refreshPageError, showApiError } from 'utils';
@ -80,6 +79,8 @@ export class AlertGroupStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/alertgroups/';
}
@ -96,13 +97,16 @@ export class AlertGroupStore extends BaseStore {
}).catch(showApiError);
}
@action
async updateItem(id: Alert['pk']) {
const item = await this.getById(id);
this.items = {
...this.items,
[item.id]: item,
};
runInAction(() => {
this.items = {
...this.items,
[item.id]: item,
};
});
}
getSearchResult(query = '') {
@ -124,12 +128,14 @@ export class AlertGroupStore extends BaseStore {
return await makeRequest(`${this.path}${pk}`, {});
}
@action
async updateSilenceOptions() {
this.silenceOptions = await makeRequest(`${this.path}silence_options/`, {});
const result = await makeRequest(`${this.path}silence_options/`, {});
runInAction(() => {
this.silenceOptions = result;
});
}
@action
async resolve(id: Alert['pk'], delay: number) {
await makeRequest(`${this.path}${id}/silence/`, {
method: 'POST',
@ -137,28 +143,24 @@ export class AlertGroupStore extends BaseStore {
});
}
@action
async unresolve(id: Alert['pk']) {
await makeRequest(`${this.path}${id}/unresolve/`, {
method: 'POST',
});
}
@action
async acknowledge(id: Alert['pk']) {
await makeRequest(`${this.path}${id}/acknowledge/`, {
method: 'POST',
});
}
@action
async unacknowledge(id: Alert['pk']) {
await makeRequest(`${this.path}${id}/unacknowledge/`, {
method: 'POST',
});
}
@action
async silence(id: Alert['pk'], delay: number) {
await makeRequest(`${this.path}${id}/silence/`, {
method: 'POST',
@ -166,7 +168,6 @@ export class AlertGroupStore extends BaseStore {
});
}
@action
async unsilence(id: Alert['pk']) {
await makeRequest(`${this.path}${id}/unsilence/`, {
method: 'POST',
@ -177,13 +178,15 @@ export class AlertGroupStore extends BaseStore {
async updateBulkActions() {
const response = await makeRequest(`${this.path}bulk_action_options/`, {});
this.bulkActions = response.reduce(
(acc: { [key: string]: boolean }, item: SelectOption) => ({
...acc,
[item.value]: true,
}),
{}
);
runInAction(() => {
this.bulkActions = response.reduce(
(acc: { [key: string]: boolean }, item: SelectOption) => ({
...acc,
[item.value]: true,
}),
{}
);
});
}
async bulkAction(data: any) {
@ -202,7 +205,6 @@ export class AlertGroupStore extends BaseStore {
// methods were moved from rootBaseStore.
// TODO check if methods are dublicating existing ones
@action
async updateIncidents() {
await Promise.all([
this.getNewIncidentsStats(),
@ -212,7 +214,12 @@ export class AlertGroupStore extends BaseStore {
this.updateAlertGroups(),
]);
this.liveUpdatesPaused = false;
this.setLiveUpdatesPaused(false);
}
@action
setLiveUpdatesPaused(value: boolean) {
this.liveUpdatesPaused = value;
}
@action
@ -276,17 +283,19 @@ export class AlertGroupStore extends BaseStore {
})
);
// @ts-ignore
this.alerts = new Map<number, Alert>([...this.alerts, ...newAlerts]);
runInAction(() => {
// @ts-ignore
this.alerts = new Map<number, Alert>([...this.alerts, ...newAlerts]);
this.alertsSearchResult['default'] = {
prev: prevCursor,
next: nextCursor,
results: results.map((alert: Alert) => alert.pk),
page_size,
};
this.alertsSearchResult['default'] = {
prev: prevCursor,
next: nextCursor,
results: results.map((alert: Alert) => alert.pk),
page_size,
};
this.alertGroupsLoading = false;
this.alertGroupsLoading = false;
});
}
getAlertSearchResult(query: string) {
@ -303,10 +312,11 @@ export class AlertGroupStore extends BaseStore {
};
}
@action
async getAlert(pk: Alert['pk']) {
return await makeRequest(`${this.path}${pk}`, {}).then((alert: Alert) => {
this.alerts.set(pk, alert);
runInAction(() => {
this.alerts.set(pk, alert);
});
return alert;
});
@ -324,7 +334,10 @@ export class AlertGroupStore extends BaseStore {
status: [IncidentStatus.Firing],
},
});
this.newIncidents = result;
runInAction(() => {
this.newIncidents = result;
});
}
@action
@ -336,7 +349,9 @@ export class AlertGroupStore extends BaseStore {
},
});
this.acknowledgedIncidents = result;
runInAction(() => {
this.acknowledgedIncidents = result;
});
}
@action
@ -348,7 +363,9 @@ export class AlertGroupStore extends BaseStore {
},
});
this.resolvedIncidents = result;
runInAction(() => {
this.resolvedIncidents = result;
});
}
@action
@ -360,7 +377,9 @@ export class AlertGroupStore extends BaseStore {
},
});
this.silencedIncidents = result;
runInAction(() => {
this.silencedIncidents = result;
});
}
@action
@ -371,32 +390,26 @@ export class AlertGroupStore extends BaseStore {
if (!isUndo) {
switch (action) {
case AlertAction.Acknowledge:
Mixpanel.track('Acknowledge Incident', null);
undoAction = AlertAction.unAcknowledge;
break;
case AlertAction.unAcknowledge:
Mixpanel.track('Unacknowledge Incident', null);
undoAction = AlertAction.Acknowledge;
break;
case AlertAction.Resolve:
Mixpanel.track('Resolve Incident', null);
undoAction = AlertAction.unResolve;
break;
case AlertAction.unResolve:
Mixpanel.track('Unresolve Incident', null);
undoAction = AlertAction.Resolve;
break;
case AlertAction.Silence:
Mixpanel.track('Silence Incident', null);
undoAction = AlertAction.unSilence;
break;
case AlertAction.unSilence:
Mixpanel.track('Unsilence Incident', null);
undoAction = AlertAction.Silence;
break;
}
this.liveUpdatesPaused = true;
this.setLiveUpdatesPaused(true);
}
try {
@ -424,11 +437,6 @@ export class AlertGroupStore extends BaseStore {
});
}
@action
toggleLiveUpdate(value: boolean) {
this.liveUpdatesEnabled = value;
}
async unpageUser(alertId: Alert['pk'], userId: User['pk']) {
return await makeRequest(`${this.path}${alertId}/unpage_user`, {
method: 'POST',
@ -442,11 +450,13 @@ export class AlertGroupStore extends BaseStore {
const { hidden, visible, default: isDefaultOrder } = tableSettings;
this.isDefaultColumnOrder = isDefaultOrder;
this.columns = [
...visible.map((item: AlertGroupColumn): AlertGroupColumn => ({ ...item, isVisible: true })),
...hidden.map((item: AlertGroupColumn): AlertGroupColumn => ({ ...item, isVisible: false })),
];
runInAction(() => {
this.isDefaultColumnOrder = isDefaultOrder;
this.columns = [
...visible.map((item: AlertGroupColumn): AlertGroupColumn => ({ ...item, isVisible: true })),
...hidden.map((item: AlertGroupColumn): AlertGroupColumn => ({ ...item, isVisible: false })),
];
});
}
@action
@ -462,24 +472,23 @@ export class AlertGroupStore extends BaseStore {
data: { ...columns },
});
this.isDefaultColumnOrder = isDefaultOrder;
runInAction(() => {
this.isDefaultColumnOrder = isDefaultOrder;
});
}
@action
async resetTableSettings(): Promise<void> {
return await makeRequest('/alertgroup_table_settings/reset', { method: 'POST' }).catch(() =>
openErrorNotification('There was an error resetting the table settings')
);
}
@action
async loadLabelsKeys(): Promise<Array<ApiSchemas['LabelKey']>> {
return await makeRequest(`/alertgroups/labels/keys/`, {}).catch(() =>
openErrorNotification('There was an error processing your request')
);
}
@action
async loadValuesForLabelKey(
key: ApiSchemas['LabelKey']['id'],
search = ''

View file

@ -1,5 +1,5 @@
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { Channel } from 'models/channel';
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';

View file

@ -1,5 +0,0 @@
export interface ApiTokenDTO {
pk: string;
token_name: string;
created_at: string;
}

View file

@ -1,8 +1,7 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
import { Mixpanel } from 'services/mixpanel';
import { RootStore } from 'state';
import { ApiToken } from './api_token.types';
@ -17,6 +16,8 @@ export class ApiTokenStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/tokens/';
}
@ -26,21 +27,23 @@ export class ApiTokenStore extends BaseStore {
params: { search: query },
});
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: ApiToken }, item: ApiToken) => ({
...acc,
[item.id]: item,
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: ApiToken }, item: ApiToken) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = {
...this.searchResult,
[query]: results.map((item: ApiToken) => item.id),
};
this.searchResult = {
...this.searchResult,
[query]: results.map((item: ApiToken) => item.id),
};
});
}
getSearchResult(query = '') {
@ -52,8 +55,6 @@ export class ApiTokenStore extends BaseStore {
}
async revokeApiToken(id: ApiToken['id']) {
Mixpanel.track('Revoke ApiToken', null);
return await makeRequest(`${this.path}${id}/`, {
method: 'DELETE',
});

View file

@ -1,14 +0,0 @@
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
import { TelegramChannel } from 'models/telegram_channel/telegram_channel.types';
export interface ChannelFilter {
id: string;
alert_receive_channel: AlertReceiveChannel['id'];
slack_channel_id?: SlackChannel['id'];
telegram_channel?: TelegramChannel['id'];
escalation_chain?: string;
created_at: string;
filtering_term: string;
is_default: boolean;
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
@ -19,6 +19,8 @@ export class CloudStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/cloud_users/';
}
@ -28,21 +30,23 @@ export class CloudStore extends BaseStore {
params: { page },
});
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: Cloud }, item: Cloud) => ({
...acc,
[item.id]: item,
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: Cloud }, item: Cloud) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = {
matched_users_count,
results: results.map((item: Cloud) => item.id),
};
this.searchResult = {
matched_users_count,
results: results.map((item: Cloud) => item.id),
};
});
}
getSearchResult() {
@ -70,14 +74,17 @@ export class CloudStore extends BaseStore {
@action.bound
async loadCloudConnectionStatus() {
this.cloudConnectionStatus = await this.getCloudConnectionStatus();
const result = await this.getCloudConnectionStatus();
runInAction(() => {
this.cloudConnectionStatus = result;
});
}
async getCloudConnectionStatus() {
return await makeRequest(`/cloud_connection/`, { method: 'GET' });
}
@action
async disconnectToCloud() {
return await makeRequest(`/cloud_connection/`, { method: 'DELETE' });
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable } from 'mobx';
import { UserResponders } from 'containers/AddResponders/AddResponders.types';
import { Alert } from 'models/alertgroup/alertgroup.types';
@ -24,6 +24,8 @@ export class DirectPagingStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/direct_paging/';
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
@ -25,6 +25,8 @@ export class EscalationChainStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/escalation_chains/';
}
@ -32,10 +34,12 @@ export class EscalationChainStore extends BaseStore {
async loadItem(id: EscalationChain['id'], skipErrorHandling = false): Promise<EscalationChain> {
const escalationChain = await this.getById(id, skipErrorHandling);
this.items = {
...this.items,
[id]: escalationChain,
};
runInAction(() => {
this.items = {
...this.items,
[id]: escalationChain,
};
});
return escalationChain;
}
@ -44,30 +48,36 @@ export class EscalationChainStore extends BaseStore {
async updateById(id: EscalationChain['id']) {
const response = await this.getById(id);
this.items = {
...this.items,
[id]: response,
};
runInAction(() => {
this.items = {
...this.items,
[id]: response,
};
});
}
@action
async save(id: EscalationChain['id'], data: Partial<EscalationChain>) {
const response = await super.update(id, data);
this.items = {
...this.items,
[id]: response,
};
runInAction(() => {
this.items = {
...this.items,
[id]: response,
};
});
}
@action
async updateEscalationChainDetails(id: EscalationChain['id']) {
const response = await makeRequest(`${this.path}${id}/details/`, {});
this.details = {
...this.details,
[id]: response,
};
runInAction(() => {
this.details = {
...this.details,
[id]: response,
};
});
}
@action
@ -86,10 +96,12 @@ export class EscalationChainStore extends BaseStore {
}
if (escalationChain) {
this.items = {
...this.items,
[id]: escalationChain,
};
runInAction(() => {
this.items = {
...this.items,
[id]: escalationChain,
};
});
}
return escalationChain;
@ -105,23 +117,25 @@ export class EscalationChainStore extends BaseStore {
params,
});
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: EscalationChain }, item: EscalationChain) => ({
...acc,
[item.id]: item,
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: EscalationChain }, item: EscalationChain) => ({
...acc,
[item.id]: item,
}),
{}
),
};
const key = typeof query === 'string' ? query : '';
const key = typeof query === 'string' ? query : '';
this.searchResult = {
...this.searchResult,
[key]: results.map((item: EscalationChain) => item.id),
};
this.searchResult = {
...this.searchResult,
[key]: results.map((item: EscalationChain) => item.id),
};
});
this.loading = false;
}

View file

@ -1,36 +0,0 @@
import { Channel } from 'models/channel';
import { Schedule } from 'models/schedule/schedule.types';
import { UserGroup } from 'models/user_group/user_group.types';
import { ChannelFilter } from './channel_filter';
import { ScheduleDTO } from './schedule';
import { User } from './user/user.types';
export interface EscalationPolicyType {
id: string;
notify_to_user: User['pk'] | null;
// it's option value from api/internal/v1/escalation_policies/escalation_options/
step: number;
wait_delay: string | null;
is_final: boolean;
channel_filter: ChannelFilter['id'];
notify_to_users_queue: Array<User['pk']>;
from_time: string | null;
to_time: string | null;
notify_to_schedule: ScheduleDTO['id'] | null;
notify_to_channel: Channel['id'] | null;
notify_to_group: UserGroup['id'];
notify_schedule: Schedule['id'];
}
export function prepareEscalationPolicy(value: EscalationPolicyType): EscalationPolicyType {
return {
...value,
notify_to_user: null,
wait_delay: null,
notify_to_users_queue: [],
from_time: null,
to_time: null,
notify_to_schedule: null,
};
}

View file

@ -1,11 +1,10 @@
import { get } from 'lodash-es';
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
import { EscalationPolicy } from 'models/escalation_policy/escalation_policy.types';
import { makeRequest } from 'network';
import { Mixpanel } from 'services/mixpanel';
import { RootStore } from 'state';
import { move } from 'state/helpers';
import { SelectOption } from 'state/types';
@ -31,13 +30,18 @@ export class EscalationPolicyStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/escalation_policies/';
}
@action.bound
async updateWebEscalationPolicyOptions() {
const response = await makeRequest('/escalation_policies/escalation_options/', {});
this.webEscalationChoices = response;
runInAction(() => {
this.webEscalationChoices = response;
});
}
@action.bound
@ -45,13 +49,19 @@ export class EscalationPolicyStore extends BaseStore {
const response = await makeRequest('/escalation_policies/', {
method: 'OPTIONS',
});
this.escalationChoices = get(response, 'actions.POST', []);
runInAction(() => {
this.escalationChoices = get(response, 'actions.POST', []);
});
}
@action.bound
async updateNumMinutesInWindowOptions() {
const response = await makeRequest('/escalation_policies/num_minutes_in_window_options/', {});
this.numMinutesInWindowOptions = response;
runInAction(() => {
this.numMinutesInWindowOptions = response;
});
}
@action
@ -68,15 +78,17 @@ export class EscalationPolicyStore extends BaseStore {
{}
);
this.items = {
...this.items,
...escalationPolicies,
};
runInAction(() => {
this.items = {
...this.items,
...escalationPolicies,
};
this.escalationChainToEscalationPolicy = {
...this.escalationChainToEscalationPolicy,
[escalationChainId]: response.map((escalationPolicy: EscalationPolicy) => escalationPolicy.id),
};
this.escalationChainToEscalationPolicy = {
...this.escalationChainToEscalationPolicy,
[escalationChainId]: response.map((escalationPolicy: EscalationPolicy) => escalationPolicy.id),
};
});
}
@action
@ -103,8 +115,6 @@ export class EscalationPolicyStore extends BaseStore {
@action
async moveEscalationPolicyToPosition(oldIndex: any, newIndex: any, escalationChainId: EscalationChain['id']) {
Mixpanel.track('Move EscalationPolicy', null);
const escalationPolicyId = this.escalationChainToEscalationPolicy[escalationChainId][oldIndex];
this.escalationChainToEscalationPolicy[escalationChainId] = move(

View file

@ -1,4 +1,4 @@
import { Channel } from 'models/channel';
import { Channel } from 'models/channel/channel';
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { Schedule } from 'models/schedule/schedule.types';

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { LabelKeyValue } from 'models/label/label.types';
@ -31,6 +31,8 @@ export class FiltersStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
const savedFilters = getItem(LOCAL_STORAGE_FILTERS_KEY);
if (savedFilters) {
this._globalValues = { ...savedFilters };
@ -61,10 +63,12 @@ export class FiltersStore extends BaseStore {
result.unshift({ name: 'search', type: 'search' });
}
this.options = {
...this.options,
[page]: result,
};
runInAction(() => {
this.options = {
...this.options,
[page]: result,
};
});
return result;
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { RootStore } from 'state';
@ -15,6 +15,8 @@ export class GlobalSettingStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/live_settings/';
}
@ -22,31 +24,35 @@ export class GlobalSettingStore extends BaseStore {
async updateById(id: GlobalSetting['id']) {
const response = await this.getById(id);
this.items = {
...this.items,
[id]: response,
};
runInAction(() => {
this.items = {
...this.items,
[id]: response,
};
});
}
@action
async updateItems(query = '') {
const results = await this.getAll();
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: GlobalSetting }, item: GlobalSetting) => ({
...acc,
[item.id]: item,
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: GlobalSetting }, item: GlobalSetting) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = {
...this.searchResult,
[query]: results.map((item: GlobalSetting) => item.id),
};
this.searchResult = {
...this.searchResult,
[query]: results.map((item: GlobalSetting) => item.id),
};
});
}
getSearchResult(query = '') {

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
@ -17,6 +17,8 @@ export class GrafanaTeamStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/teams/';
}
@ -24,10 +26,12 @@ export class GrafanaTeamStore extends BaseStore {
async updateTeam(id: GrafanaTeam['id'], data: Partial<GrafanaTeam>) {
const result = await this.update(id, data);
this.items = {
...this.items,
[id]: result,
};
runInAction(() => {
this.items = {
...this.items,
[id]: result,
};
});
}
@action.bound
@ -40,18 +44,21 @@ export class GrafanaTeamStore extends BaseStore {
only_include_notifiable_teams: onlyIncludeNotifiableTeams ? 'true' : 'false',
},
});
this.items = {
...this.items,
...result.reduce<TeamItems>(
(acc, item) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = result.map((item: GrafanaTeam) => item.id);
runInAction(() => {
this.items = {
...this.items,
...result.reduce<TeamItems>(
(acc, item) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = result.map((item: GrafanaTeam) => item.id);
});
}
getSearchResult() {

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import BaseStore from 'models/base_store';
@ -17,12 +17,18 @@ export class HeartbeatStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/heartbeats/';
}
@action
async updateTimeoutOptions() {
this.timeoutOptions = await makeRequest(`${this.path}timeout_options/`, {});
const result = await makeRequest(`${this.path}timeout_options/`, {});
runInAction(() => {
this.timeoutOptions = result;
});
}
@action
@ -33,10 +39,12 @@ export class HeartbeatStore extends BaseStore {
return;
}
this.items = {
...this.items,
[response.id]: response,
};
runInAction(() => {
this.items = {
...this.items,
[response.id]: response,
};
});
}
@action
@ -50,14 +58,16 @@ export class HeartbeatStore extends BaseStore {
return;
}
this.rootStore.alertReceiveChannelStore.alertReceiveChannelToHeartbeat = {
...this.rootStore.alertReceiveChannelStore.alertReceiveChannelToHeartbeat,
[alertReceiveChannelId]: response.id,
};
runInAction(() => {
this.rootStore.alertReceiveChannelStore.alertReceiveChannelToHeartbeat = {
...this.rootStore.alertReceiveChannelStore.alertReceiveChannelToHeartbeat,
[alertReceiveChannelId]: response.id,
};
this.items = {
...this.items,
[response.id]: response,
};
this.items = {
...this.items,
[response.id]: response,
};
});
}
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
@ -17,13 +17,18 @@ export class LabelStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/labels/';
}
@action.bound
public async loadKeys() {
const { data } = await onCallApi.GET('/labels/keys/', undefined);
this.keys = data;
runInAction(() => {
this.keys = data;
});
return data;
}
@ -40,15 +45,16 @@ export class LabelStore extends BaseStore {
const filteredValues = result.values.filter((v) => v.name.toLowerCase().includes(search.toLowerCase())); // TODO remove after backend search implementation
this.values = {
...this.values,
[key]: filteredValues,
};
runInAction(() => {
this.values = {
...this.values,
[key]: filteredValues,
};
});
return { ...result, values: filteredValues };
}
@action.bound
@WithGlobalNotification({ success: 'New key has been added', failure: 'Failed to add new key' })
async createKey(name: string) {
const data = await makeRequest(`${this.path}`, {
@ -58,7 +64,6 @@ export class LabelStore extends BaseStore {
return data.key;
}
@action.bound
@WithGlobalNotification({ success: 'New value has been added', failure: 'Failed to add new value' })
async createValue(keyId: ApiSchemas['LabelKey']['id'], value: string) {
const result = await makeRequest(`${this.path}id/${keyId}/values`, {
@ -68,7 +73,6 @@ export class LabelStore extends BaseStore {
return result.values.find((v) => v.name === value); // TODO remove after backend API change
}
@action.bound
@WithGlobalNotification({ success: 'Key has been renamed', failure: 'Failed to rename key' })
async updateKey(keyId: ApiSchemas['LabelKey']['id'], name: string) {
const result = await makeRequest(`${this.path}id/${keyId}`, {
@ -78,7 +82,6 @@ export class LabelStore extends BaseStore {
return result.key;
}
@action.bound
@WithGlobalNotification({ success: 'Value has been renamed', failure: 'Failed to rename value' })
async updateKeyValue(keyId: ApiSchemas['LabelKey']['id'], valueId: ApiSchemas['LabelValue']['id'], name: string) {
const result = await makeRequest(`${this.path}id/${keyId}/values/${valueId}`, {

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable } from 'mobx';
interface LoadingResult {
[key: string]: boolean;
@ -8,6 +8,10 @@ class LoaderStoreClass {
@observable
items: LoadingResult = {};
constructor() {
makeObservable(this);
}
@action
setLoadingAction(actionKey: string, isLoading: boolean) {
this.items[actionKey] = isLoading;

View file

@ -1,4 +1,4 @@
import { User } from './user/user.types';
import { User } from 'models/user/user.types';
export interface NotificationPolicyType {
id: string;

View file

@ -1,4 +0,0 @@
export interface NotifyBy {
value: string;
display_name: string;
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
@ -12,16 +12,19 @@ export class OrganizationStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/organization/';
}
@action.bound
async loadCurrentOrganization() {
const organization = await makeRequest(this.path, {});
this.currentOrganization = organization;
runInAction(() => {
this.currentOrganization = organization;
});
}
@action
async saveCurrentOrganization(data: Partial<Organization>) {
this.currentOrganization = await makeRequest(this.path, {
method: 'PUT',

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { LabelsErrors } from 'models/label/label.types';
@ -23,6 +23,8 @@ export class OutgoingWebhookStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/webhooks/';
}
@ -30,10 +32,12 @@ export class OutgoingWebhookStore extends BaseStore {
async loadItem(id: OutgoingWebhook['id'], skipErrorHandling = false): Promise<OutgoingWebhook> {
const outgoingWebhook = await this.getById(id, skipErrorHandling);
this.items = {
...this.items,
[id]: outgoingWebhook,
};
runInAction(() => {
this.items = {
...this.items,
[id]: outgoingWebhook,
};
});
return outgoingWebhook;
}
@ -42,19 +46,24 @@ export class OutgoingWebhookStore extends BaseStore {
async updateById(id: OutgoingWebhook['id']) {
const response = await this.getById(id);
this.items = {
...this.items,
[id]: response,
};
runInAction(() => {
this.items = {
...this.items,
[id]: response,
};
});
}
@action
async updateItem(id: OutgoingWebhook['id'], fromOrganization = false) {
const response = await this.getById(id, false, fromOrganization);
this.items = {
...this.items,
[id]: response,
};
runInAction(() => {
this.items = {
...this.items,
[id]: response,
};
});
}
@action
@ -65,23 +74,25 @@ export class OutgoingWebhookStore extends BaseStore {
params,
});
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: OutgoingWebhook }, item: OutgoingWebhook) => ({
...acc,
[item.id]: item,
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: OutgoingWebhook }, item: OutgoingWebhook) => ({
...acc,
[item.id]: item,
}),
{}
),
};
const key = typeof query === 'string' ? query : '';
const key = typeof query === 'string' ? query : '';
this.searchResult = {
...this.searchResult,
[key]: results.map((item: OutgoingWebhook) => item.id),
};
this.searchResult = {
...this.searchResult,
[key]: results.map((item: OutgoingWebhook) => item.id),
};
});
}
getSearchResult(query = '') {
@ -108,7 +119,10 @@ export class OutgoingWebhookStore extends BaseStore {
@action.bound
async updateOutgoingWebhookPresetsOptions() {
const response = await makeRequest(`/webhooks/preset_options/`, {});
this.outgoingWebhookPresets = response;
runInAction(() => {
this.outgoingWebhookPresets = response;
});
}
@action.bound

View file

@ -1,16 +1,9 @@
import { observable } from 'mobx';
import { Alert } from 'models/alertgroup/alertgroup.types';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
import { RootStore } from 'state';
import { ResolutionNote } from './resolution_note.types';
export class ResolutionNotesStore extends BaseStore {
@observable.shallow
resolutionNotes: { [id: string]: ResolutionNote[] } = {};
constructor(rootStore: RootStore) {
super(rootStore);

View file

@ -1,14 +0,0 @@
export interface ScheduleDTO {
id: string;
name: string;
ical_url_primary: string;
ical_url_overrides: string;
type: ScheduleType;
channel_name: string;
channel: string;
}
export enum ScheduleType {
CalendarSchedule,
IcalSchedule,
}

View file

@ -1,5 +1,5 @@
import dayjs from 'dayjs';
import { action, observable } from 'mobx';
import { action, makeObservable, observable, runInAction } from 'mobx';
import { RemoteFiltersType } from 'containers/RemoteFilters/RemoteFilters.types';
import BaseStore from 'models/base_store';
@ -118,6 +118,8 @@ export class ScheduleStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/schedules/';
}
@ -125,10 +127,12 @@ export class ScheduleStore extends BaseStore {
async loadItem(id: Schedule['id'], skipErrorHandling = false): Promise<Schedule> {
const schedule = await this.getById(id, skipErrorHandling);
this.items = {
...this.items,
[id]: schedule,
};
runInAction(() => {
this.items = {
...this.items,
[id]: schedule,
};
});
return schedule;
}
@ -149,23 +153,26 @@ export class ScheduleStore extends BaseStore {
return;
}
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: Schedule }, item: Schedule) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = {
page_size,
count,
results: results.map((item: Schedule) => item.id),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: Schedule }, item: Schedule) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = {
page_size,
count,
results: results.map((item: Schedule) => item.id),
};
});
}
@action
async updateItem(id: Schedule['id'], fromOrganization = false) {
if (id) {
let schedule;
@ -182,10 +189,12 @@ export class ScheduleStore extends BaseStore {
}
if (schedule) {
this.items = {
...this.items,
[id]: schedule,
};
runInAction(() => {
this.items = {
...this.items,
[id]: schedule,
};
});
}
return schedule;
@ -204,7 +213,6 @@ export class ScheduleStore extends BaseStore {
return await makeRequest(`/schedules/${scheduleId}/quality`, { method: 'GET' });
}
@action
async reloadIcal(scheduleId: Schedule['id']) {
await makeRequest(`/schedules/${scheduleId}/reload_ical/`, {
method: 'POST',
@ -231,6 +239,7 @@ export class ScheduleStore extends BaseStore {
// ------- NEW SCHEDULES API ENDPOINTS ---------
@action
async createRotation(scheduleId: Schedule['id'], isOverride: boolean, params: Partial<Shift>) {
const type = isOverride ? 3 : 2;
@ -239,10 +248,12 @@ export class ScheduleStore extends BaseStore {
method: 'POST',
});
this.shifts = {
...this.shifts,
[response.id]: response,
};
runInAction(() => {
this.shifts = {
...this.shifts,
[response.id]: response,
};
});
return response;
}
@ -270,26 +281,28 @@ export class ScheduleStore extends BaseStore {
method: 'POST',
});
if (isOverride) {
const overridePreview = enrichOverrides(
[...(this.events[scheduleId]?.['override']?.[fromString] as Array<{ shiftId: string; events: Event[] }>)],
response.rotation,
shiftId
);
runInAction(() => {
if (isOverride) {
const overridePreview = enrichOverrides(
[...(this.events[scheduleId]?.['override']?.[fromString] as Array<{ shiftId: string; events: Event[] }>)],
response.rotation,
shiftId
);
this.overridePreview = { ...this.overridePreview, [fromString]: overridePreview };
} else {
const layers = enrichLayers(
[...(this.events[scheduleId]?.['rotation']?.[fromString] as Layer[])],
response.rotation,
shiftId,
params.priority_level
);
this.overridePreview = { ...this.overridePreview, [fromString]: overridePreview };
} else {
const layers = enrichLayers(
[...(this.events[scheduleId]?.['rotation']?.[fromString] as Layer[])],
response.rotation,
shiftId,
params.priority_level
);
this.rotationPreview = { ...this.rotationPreview, [fromString]: layers };
}
this.rotationPreview = { ...this.rotationPreview, [fromString]: layers };
}
this.finalPreview = { ...this.finalPreview, [fromString]: fillGapsInShifts(splitToShifts(response.final)) };
this.finalPreview = { ...this.finalPreview, [fromString]: fillGapsInShifts(splitToShifts(response.final)) };
});
}
@action
@ -310,10 +323,12 @@ export class ScheduleStore extends BaseStore {
const shiftEventsListFlattened = flattenShiftEvents([...existingShiftEventsList, newShiftEvents]);
this.shiftSwapsPreview = {
...this.shiftSwapsPreview,
[fromString]: shiftEventsListFlattened,
};
runInAction(() => {
this.shiftSwapsPreview = {
...this.shiftSwapsPreview,
[fromString]: shiftEventsListFlattened,
};
});
}
@action
@ -325,6 +340,7 @@ export class ScheduleStore extends BaseStore {
this.rotationFormLiveParams = undefined;
}
@action
async updateRotation(shiftId: Shift['id'], params: Partial<Shift>) {
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
params: { force: true },
@ -332,54 +348,66 @@ export class ScheduleStore extends BaseStore {
method: 'PUT',
});
this.shifts = {
...this.shifts,
[response.id]: response,
};
runInAction(() => {
this.shifts = {
...this.shifts,
[response.id]: response,
};
});
return response;
}
@action
async updateRotationAsNew(shiftId: Shift['id'], params: Partial<Shift>) {
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
data: { ...params },
method: 'PUT',
});
this.shifts = {
...this.shifts,
[response.id]: response,
};
runInAction(() => {
this.shifts = {
...this.shifts,
[response.id]: response,
};
});
return response;
}
@action
updateRelatedEscalationChains = async (id: Schedule['id']) => {
const response = await makeRequest(`/schedules/${id}/related_escalation_chains`, {
method: 'GET',
});
this.relatedEscalationChains = {
...this.relatedEscalationChains,
[id]: response,
};
runInAction(() => {
this.relatedEscalationChains = {
...this.relatedEscalationChains,
[id]: response,
};
});
return response;
};
@action
updateRelatedUsers = async (id: Schedule['id']) => {
const { users } = await makeRequest(`/schedules/${id}/next_shifts_per_user`, {
method: 'GET',
});
this.relatedUsers = {
...this.relatedUsers,
[id]: users,
};
runInAction(() => {
this.relatedUsers = {
...this.relatedUsers,
[id]: users,
};
});
return users;
};
@action
async updateOncallShifts(scheduleId: Schedule['id']) {
const { results } = await makeRequest(`/oncall_shifts/`, {
params: {
@ -388,16 +416,18 @@ export class ScheduleStore extends BaseStore {
method: 'GET',
});
this.shifts = {
...this.shifts,
...results.reduce(
(acc: { [key: number]: Shift }, item: Shift) => ({
...acc,
[item.id]: item,
}),
{}
),
};
runInAction(() => {
this.shifts = {
...this.shifts,
...results.reduce(
(acc: { [key: number]: Shift }, item: Shift) => ({
...acc,
[item.id]: item,
}),
{}
),
};
});
}
@action
@ -410,10 +440,12 @@ export class ScheduleStore extends BaseStore {
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {});
this.shifts = {
...this.shifts,
[shiftId]: response,
};
runInAction(() => {
this.shifts = {
...this.shifts,
[shiftId]: response,
};
});
delete this.shiftsCurrentlyUpdating[shiftId];
@ -424,10 +456,12 @@ export class ScheduleStore extends BaseStore {
async saveOncallShift(shiftId: Shift['id'], data: Partial<Shift>) {
const response = await makeRequest(`/oncall_shifts/${shiftId}`, { method: 'PUT', data });
this.shifts = {
...this.shifts,
[shiftId]: response,
};
runInAction(() => {
this.shifts = {
...this.shifts,
[shiftId]: response,
};
});
return response;
}
@ -439,6 +473,7 @@ export class ScheduleStore extends BaseStore {
}).catch(this.onApiError);
}
@action
async updateEvents(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, type: RotationType = 'rotation', days = 9) {
const dayBefore = startMoment.subtract(1, 'day');
@ -457,16 +492,18 @@ export class ScheduleStore extends BaseStore {
const shifts = fillGapsInShifts(shiftsUnflattened);
const layers = type === 'rotation' ? splitToLayers(shifts) : undefined;
this.events = {
...this.events,
[scheduleId]: {
...this.events[scheduleId],
[type]: {
...this.events[scheduleId]?.[type],
[fromString]: layers ? layers : shifts,
runInAction(() => {
this.events = {
...this.events,
[scheduleId]: {
...this.events[scheduleId],
[type]: {
...this.events[scheduleId]?.[type],
[fromString]: layers ? layers : shifts,
},
},
},
};
};
});
}
async updateFrequencyOptions() {
@ -475,10 +512,15 @@ export class ScheduleStore extends BaseStore {
});
}
@action
async updateDaysOptions() {
this.byDayOptions = await makeRequest(`/oncall_shifts/days_options/`, {
const result = await makeRequest(`/oncall_shifts/days_options/`, {
method: 'GET',
});
runInAction(() => {
this.byDayOptions = result;
});
}
async createShiftSwap(params: Partial<ShiftSwap>) {
@ -493,14 +535,18 @@ export class ScheduleStore extends BaseStore {
return await makeRequest(`/shift_swaps/${shiftSwapId}/take`, { method: 'POST' }).catch(this.onApiError);
}
@action
async loadShiftSwap(id: ShiftSwap['id']) {
const result = await makeRequest(`/shift_swaps/${id}`, { params: { expand_users: true } });
this.shiftSwaps = { ...this.shiftSwaps, [id]: result };
runInAction(() => {
this.shiftSwaps = { ...this.shiftSwaps, [id]: result };
});
return result;
}
@action
async updateShiftSwaps(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, days = 9) {
const fromString = getFromString(startMoment);
@ -522,23 +568,26 @@ export class ScheduleStore extends BaseStore {
const shiftEventsListFlattened = flattenShiftEvents(shiftEventsList);
this.shiftSwaps = result.shift_swaps.reduce(
(memo, shiftSwap) => ({
...memo,
[shiftSwap.id]: shiftSwap,
}),
this.shiftSwaps
);
runInAction(() => {
this.shiftSwaps = result.shift_swaps.reduce(
(memo, shiftSwap) => ({
...memo,
[shiftSwap.id]: shiftSwap,
}),
this.shiftSwaps
);
this.scheduleAndDateToShiftSwaps = {
...this.scheduleAndDateToShiftSwaps,
[scheduleId]: {
...this.scheduleAndDateToShiftSwaps[scheduleId],
[fromString]: shiftEventsListFlattened,
},
};
this.scheduleAndDateToShiftSwaps = {
...this.scheduleAndDateToShiftSwaps,
[scheduleId]: {
...this.scheduleAndDateToShiftSwaps[scheduleId],
[fromString]: shiftEventsListFlattened,
},
};
});
}
@action
async updatePersonalEvents(userPk: User['pk'], startMoment: dayjs.Dayjs, days = 9, isUpdateOnCallNow = false) {
const fromString = getFromString(startMoment);
@ -558,20 +607,22 @@ export class ScheduleStore extends BaseStore {
const shiftEventsListFlattened = flattenShiftEvents(shiftEventsList);
this.personalEvents = {
...this.personalEvents,
[userPk]: {
...this.personalEvents[userPk],
[fromString]: shiftEventsListFlattened,
},
};
if (isUpdateOnCallNow) {
// since current endpoint works incorrectly we are waiting for https://github.com/grafana/oncall/issues/3164
this.onCallNow = {
...this.onCallNow,
[userPk]: is_oncall,
runInAction(() => {
this.personalEvents = {
...this.personalEvents,
[userPk]: {
...this.personalEvents[userPk],
[fromString]: shiftEventsListFlattened,
},
};
}
if (isUpdateOnCallNow) {
// since current endpoint works incorrectly we are waiting for https://github.com/grafana/oncall/issues/3164
this.onCallNow = {
...this.onCallNow,
[userPk]: is_oncall,
};
}
});
}
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
@ -16,19 +16,28 @@ export class SlackStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
}
@action
async updateSlackSettings() {
this.slackSettings = await makeRequest('/slack_settings/', {});
const result = await makeRequest('/slack_settings/', {});
runInAction(() => {
this.slackSettings = result;
});
}
@action
async saveSlackSettings(data: Partial<SlackSettings>) {
this.slackSettings = await makeRequest('/slack_settings/', {
const result = await makeRequest('/slack_settings/', {
data,
method: 'PUT',
});
runInAction(() => {
this.slackSettings = result;
});
}
@action
@ -41,12 +50,17 @@ export class SlackStore extends BaseStore {
@action
async updateSlackIntegrationData(slack_id: string) {
return (this.slackIntegrationData = await makeRequest('/slack_integration/', {
const result = await makeRequest('/slack_integration/', {
params: { slack_id },
}));
});
runInAction(() => {
this.slackIntegrationData = result;
});
return result;
}
@action
async reinstallSlackIntegration(slack_id: string) {
return await makeRequest('/slack_integration/', {
validateStatus: function (status) {
@ -57,7 +71,6 @@ export class SlackStore extends BaseStore {
}).catch(this.onApiError);
}
@action
async slackLogin() {
const url_for_redirect = await makeRequest('/login/slack-login/', {});
window.location = url_for_redirect;

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
@ -16,6 +16,8 @@ export class SlackChannelStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/slack_channels/';
}
@ -23,20 +25,24 @@ export class SlackChannelStore extends BaseStore {
async updateById(id: SlackChannel['id']) {
const response = await this.getById(id);
this.items = {
...this.items,
[id]: response,
};
runInAction(() => {
this.items = {
...this.items,
[id]: response,
};
});
}
@action
async updateItem(id: SlackChannel['id']) {
const response = await this.getById(id);
this.items = {
...this.items,
[id]: response,
};
runInAction(() => {
this.items = {
...this.items,
[id]: response,
};
});
}
@action
@ -45,21 +51,23 @@ export class SlackChannelStore extends BaseStore {
params: { search: query },
});
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: SlackChannel }, item: SlackChannel) => ({
...acc,
[item.id]: item,
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: SlackChannel }, item: SlackChannel) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = {
...this.searchResult,
[query]: results.map((item: SlackChannel) => item.id),
};
this.searchResult = {
...this.searchResult,
[query]: results.map((item: SlackChannel) => item.id),
};
});
}
getSearchResult(query = '') {

View file

@ -1,4 +1,4 @@
import { action, computed, observable } from 'mobx';
import { action, computed, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
@ -21,6 +21,8 @@ export class TelegramChannelStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/telegram_channels/';
}
@ -36,43 +38,49 @@ export class TelegramChannelStore extends BaseStore {
{}
);
this.items = {
...this.items,
...items,
};
runInAction(() => {
this.items = {
...this.items,
...items,
};
this.currentTeamToTelegramChannel = response.map((telegramChannel: TelegramChannel) => telegramChannel.id);
this.currentTeamToTelegramChannel = response.map((telegramChannel: TelegramChannel) => telegramChannel.id);
});
}
@action
async updateById(id: TelegramChannel['id']) {
const response = await this.getById(id);
this.items = {
...this.items,
[id]: response,
};
runInAction(() => {
this.items = {
...this.items,
[id]: response,
};
});
}
@action
async updateItems(query = '') {
const result = await this.getAll();
this.items = {
...this.items,
...result.reduce(
(acc: { [key: number]: TelegramChannel }, item: TelegramChannel) => ({
...acc,
[item.id]: item,
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...result.reduce(
(acc: { [key: number]: TelegramChannel }, item: TelegramChannel) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = {
...this.searchResult,
[query]: result.map((item: TelegramChannel) => item.id),
};
this.searchResult = {
...this.searchResult,
[query]: result.map((item: TelegramChannel) => item.id),
};
});
}
getSearchResult(query = '') {

View file

@ -1,12 +1,11 @@
import { config } from '@grafana/runtime';
import dayjs from 'dayjs';
import { get } from 'lodash-es';
import { action, computed, observable } from 'mobx';
import { action, computed, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { NotificationPolicyType } from 'models/notification_policy';
import { NotificationPolicyType } from 'models/notification_policy/notification_policy';
import { makeRequest } from 'network';
import { Mixpanel } from 'services/mixpanel';
import { RootStore } from 'state';
import { move } from 'state/helpers';
import { throttlingError } from 'utils';
@ -48,6 +47,8 @@ export class UserStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/users/';
}
@ -64,11 +65,13 @@ export class UserStore extends BaseStore {
const response = await makeRequest('/user/', {});
const timezone = await this.refreshTimezone(response.pk);
this.items = {
...this.items,
[response.pk]: { ...response, timezone },
};
this.currentUserPk = response.pk;
runInAction(() => {
this.items = {
...this.items,
[response.pk]: { ...response, timezone },
};
this.currentUserPk = response.pk;
});
}
@action
@ -88,10 +91,12 @@ export class UserStore extends BaseStore {
async loadUser(userPk: User['pk'], skipErrorHandling = false): Promise<User> {
const user = await this.getById(userPk, skipErrorHandling);
this.items = {
...this.items,
[user.pk]: { ...user, timezone: getTimezone(user) },
};
runInAction(() => {
this.items = {
...this.items,
[user.pk]: { ...user, timezone: getTimezone(user) },
};
});
return user;
}
@ -106,10 +111,12 @@ export class UserStore extends BaseStore {
const user = await this.getById(userPk);
this.items = {
...this.items,
[user.pk]: { ...user, timezone: getTimezone(user) },
};
runInAction(() => {
this.items = {
...this.items,
[user.pk]: { ...user, timezone: getTimezone(user) },
};
});
delete this.itemsCurrentlyUpdating[userPk];
}
@ -135,25 +142,27 @@ export class UserStore extends BaseStore {
const { count, results, page_size } = response;
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: User }, item: User) => ({
...acc,
[item.pk]: {
...item,
timezone: getTimezone(item),
},
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: User }, item: User) => ({
...acc,
[item.pk]: {
...item,
timezone: getTimezone(item),
},
}),
{}
),
};
this.searchResult = {
count,
page_size,
results: results.map((item: User) => item.pk),
};
this.searchResult = {
count,
page_size,
results: results.map((item: User) => item.pk),
};
});
return response;
}
@ -178,10 +187,12 @@ export class UserStore extends BaseStore {
const user = await this.getById(userPk);
this.items = {
...this.items,
[user.pk]: user,
};
runInAction(() => {
this.items = {
...this.items,
[user.pk]: user,
};
});
};
@action
@ -192,10 +203,12 @@ export class UserStore extends BaseStore {
const user = await this.getById(userPk);
this.items = {
...this.items,
[user.pk]: user,
};
runInAction(() => {
this.items = {
...this.items,
[user.pk]: user,
};
});
};
sendBackendConfirmationCode = (userPk: User['pk'], backend: string) =>
@ -216,10 +229,12 @@ export class UserStore extends BaseStore {
async createUser(data: any) {
const user = await this.create(data);
this.items = {
...this.items,
[user.pk]: user,
};
runInAction(() => {
this.items = {
...this.items,
[user.pk]: user,
};
});
return user;
}
@ -238,10 +253,12 @@ export class UserStore extends BaseStore {
this.rootStore.userStore.loadCurrentUser();
}
this.items = {
...this.items,
[data.pk as User['pk']]: user,
};
runInAction(() => {
this.items = {
...this.items,
[data.pk as User['pk']]: user,
};
});
}
@action
@ -254,10 +271,12 @@ export class UserStore extends BaseStore {
},
});
this.items = {
...this.items,
[this.currentUserPk as User['pk']]: user,
};
runInAction(() => {
this.items = {
...this.items,
[this.currentUserPk as User['pk']]: user,
};
});
}
@action
@ -300,15 +319,16 @@ export class UserStore extends BaseStore {
params: { user: id, important: false },
});
this.notificationPolicies = {
...this.notificationPolicies,
[id]: [...nonImportantEPs, ...importantEPs],
};
runInAction(() => {
this.notificationPolicies = {
...this.notificationPolicies,
[id]: [...nonImportantEPs, ...importantEPs],
};
});
}
@action
async moveNotificationPolicyToPosition(userPk: User['pk'], oldIndex: number, newIndex: number, offset: number) {
Mixpanel.track('Move NotificationPolicy', null);
const notificationPolicy = this.notificationPolicies[userPk][oldIndex + offset];
this.notificationPolicies[userPk] = move(this.notificationPolicies[userPk], oldIndex + offset, newIndex + offset);
@ -348,20 +368,20 @@ export class UserStore extends BaseStore {
data: value,
});
this.notificationPolicies = {
...this.notificationPolicies,
[userPk]: this.notificationPolicies[userPk].map((policy: NotificationPolicyType) =>
id === policy.id ? { ...policy, ...notificationPolicy } : policy
),
};
runInAction(() => {
this.notificationPolicies = {
...this.notificationPolicies,
[userPk]: this.notificationPolicies[userPk].map((policy: NotificationPolicyType) =>
id === policy.id ? { ...policy, ...notificationPolicy } : policy
),
};
});
this.updateItem(userPk); // to update notification_chain_verbal
}
@action
async deleteNotificationPolicy(userPk: User['pk'], id: NotificationPolicyType['id']) {
Mixpanel.track('Delete NotificationPolicy', null);
await makeRequest(`/notification_policies/${id}`, { method: 'DELETE' }).catch(this.onApiError);
this.updateNotificationPolicies(userPk);
@ -374,7 +394,10 @@ export class UserStore extends BaseStore {
const response = await makeRequest('/notification_policies/', {
method: 'OPTIONS',
});
this.notificationChoices = get(response, 'actions.POST', []);
runInAction(() => {
this.notificationChoices = get(response, 'actions.POST', []);
});
}
@action
@ -390,9 +413,13 @@ export class UserStore extends BaseStore {
@action.bound
async updateNotifyByOptions() {
const response = await makeRequest('/notification_policies/notify_by_options/', {});
this.notifyByOptions = response;
runInAction(() => {
this.notifyByOptions = response;
});
}
@action
async makeTestCall(userPk: User['pk']) {
this.isTestCallInProgress = true;
@ -401,7 +428,9 @@ export class UserStore extends BaseStore {
})
.catch(this.onApiError)
.finally(() => {
this.isTestCallInProgress = false;
runInAction(() => {
this.isTestCallInProgress = false;
});
});
}

View file

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable, runInAction } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
@ -16,6 +16,8 @@ export class UserGroupStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
this.path = '/user_groups/';
}
@ -25,21 +27,23 @@ export class UserGroupStore extends BaseStore {
params: { search: query },
});
this.items = {
...this.items,
...result.reduce(
(acc: { [key: number]: UserGroup }, item: UserGroup) => ({
...acc,
[item.id]: item,
}),
{}
),
};
runInAction(() => {
this.items = {
...this.items,
...result.reduce(
(acc: { [key: number]: UserGroup }, item: UserGroup) => ({
...acc,
[item.id]: item,
}),
{}
),
};
this.searchResult = {
...(this.searchResult || {}),
[query]: result.map((item: UserGroup) => item.id),
};
this.searchResult = {
...(this.searchResult || {}),
[query]: result.map((item: UserGroup) => item.id),
};
});
}
getSearchResult(query = '') {

View file

@ -1,4 +0,0 @@
export interface WaitDelay {
value: string;
display_name: string;
}

View file

@ -179,9 +179,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
);
}
renderCards(filtersState, setFiltersState, filtersOnFiltersValueChange) {
const { store } = this.props;
renderCards(filtersState, setFiltersState, filtersOnFiltersValueChange, store) {
const { values } = filtersState;
const { newIncidents, acknowledgedIncidents, resolvedIncidents, silencedIncidents } = store.alertGroupStore;
@ -301,7 +299,9 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
query={query}
page={PAGE.Incidents}
onChange={this.handleFiltersChange}
extraFilters={this.renderCards.bind(this)}
extraFilters={(...args) => {
return this.renderCards(...args, store);
}}
grafanaTeamStore={store.grafanaTeamStore}
defaultFilters={{
team: [],

View file

@ -56,8 +56,8 @@ import {
AlertReceiveChannel,
AlertReceiveChannelCounters,
} from 'models/alert_receive_channel/alert_receive_channel.types';
import { AlertTemplatesDTO } from 'models/alert_templates';
import { ChannelFilter } from 'models/channel_filter';
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { INTEGRATION_TEMPLATES_LIST } from 'pages/integration/Integration.config';
import IntegrationHelper from 'pages/integration/Integration.helper';
import styles from 'pages/integration/Integration.module.scss';

View file

@ -2,9 +2,8 @@ import React from 'react';
import { Button, Checkbox, HorizontalGroup, Icon } from '@grafana/ui';
import cn from 'classnames/bind';
import { observe } from 'mobx';
import { Lambda, observe } from 'mobx';
import { observer } from 'mobx-react';
import { Lambda } from 'mobx/lib/internal';
import GTable from 'components/GTable/GTable';
import Text from 'components/Text/Text';

View file

@ -1,28 +0,0 @@
const mixpanel = window.mixpanel;
let actions = {
identify: (id: any) => {
if (mixpanel) {
mixpanel.identify(id);
}
},
alias: (id: any) => {
if (mixpanel) {
mixpanel.alias(id);
}
},
track: (name: any, props: any) => {
if (mixpanel) {
mixpanel.track(name, props);
}
},
people: {
set: (props: any) => {
if (mixpanel) {
mixpanel.people.set(props);
}
},
},
};
export let Mixpanel = actions;

View file

@ -1,6 +1,6 @@
import { locationService } from '@grafana/runtime';
import { contextSrv } from 'grafana/app/core/core';
import { action, observable } from 'mobx';
import { action, makeObservable, observable, runInAction } from 'mobx';
import moment from 'moment-timezone';
import qs from 'query-string';
import { OnCallAppPluginMeta } from 'types';
@ -113,8 +113,11 @@ export class RootBaseStore {
labelsStore = new LabelStore(this);
loaderStore = LoaderStore;
constructor() {
makeObservable(this);
}
@action.bound
async loadBasicData() {
loadBasicData = async () => {
const updateFeatures = async () => {
await this.updateFeatures();
@ -131,18 +134,24 @@ export class RootBaseStore {
() => this.grafanaTeamStore.updateItems(),
() => updateFeatures(),
]);
this.isBasicDataLoaded = true;
}
this.setIsBasicDataLoaded(true);
};
@action.bound
async loadMasterData() {
@action
loadMasterData = async () => {
Promise.all([
this.userStore.updateNotificationPolicyOptions(),
this.userStore.updateNotifyByOptions(),
this.alertReceiveChannelStore.updateAlertReceiveChannelOptions(),
]);
};
@action
setIsBasicDataLoaded(value: boolean) {
this.isBasicDataLoaded = value;
}
@action
setupPluginError(errorMsg: string) {
this.initializationError = errorMsg;
}
@ -167,7 +176,7 @@ export class RootBaseStore {
* Finally, try to load the current user from the OnCall backend
*/
async setupPlugin(meta: OnCallAppPluginMeta) {
this.initializationError = null;
this.setupPluginError(null);
this.onCallApiUrl = getOnCallApiUrl(meta);
if (!FaroHelper.faro) {
@ -245,9 +254,11 @@ export class RootBaseStore {
}
} else {
// everything is all synced successfully at this point..
this.backendVersion = pluginConnectionStatus.version;
this.backendLicense = pluginConnectionStatus.license;
this.recaptchaSiteKey = pluginConnectionStatus.recaptcha_site_key;
runInAction(() => {
this.backendVersion = pluginConnectionStatus.version;
this.backendLicense = pluginConnectionStatus.license;
this.recaptchaSiteKey = pluginConnectionStatus.recaptcha_site_key;
});
}
if (!this.userStore.currentUser) {
@ -292,13 +303,16 @@ export class RootBaseStore {
@action.bound
async updateFeatures() {
const response = await makeRequest('/features/', {});
this.features = response.reduce(
(acc: any, key: string) => ({
...acc,
[key]: true,
}),
{}
);
runInAction(() => {
this.features = response.reduce(
(acc: any, key: string) => ({
...acc,
[key]: true,
}),
{}
);
});
}
@action

View file

@ -11,6 +11,7 @@
"strict": false,
"resolveJsonModule": true,
"noImplicitAny": false,
"skipLibCheck": true
"skipLibCheck": true,
"useDefineForClassFields": true
}
}

View file

@ -11101,22 +11101,24 @@ mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mobx-react-lite@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-1.4.0.tgz#193beb5fdddf17ae61542f65ff951d84db402351"
integrity sha512-5xCuus+QITQpzKOjAOIQ/YxNhOl/En+PlNJF+5QU4Qxn9gnNMJBbweAdEW3HnuVQbfqDYEUnkGs5hmkIIStehg==
mobx-react@6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.1.1.tgz#24a2c8a3393890fa732b4efd34cc6dcccf6e0e7a"
integrity sha512-hjACWCTpxZf9Sv1YgWF/r6HS6Nsly1SYF22qBJeUE3j+FMfoptgjf8Zmcx2d6uzA07Cezwap5Cobq9QYa0MKUw==
mobx-react-lite@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.5.tgz#e2cb98f813e118917bcc463638f5bf6ea053a67b"
integrity sha512-StfB2wxE8imKj1f6T8WWPf4lVMx3cYH9Iy60bbKXEs21+HQ4tvvfIBZfSmMXgQAefi8xYEwQIz4GN9s0d2h7dg==
dependencies:
mobx-react-lite "1.4.0"
use-sync-external-store "^1.2.0"
mobx@5.13.0:
version "5.13.0"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.13.0.tgz#0fd68f10aa5ff2d146a4ed9e145b53337cfbca59"
integrity sha512-eSAntMSMNj0PFL705rgv+aB/z1RjNqDnFEpBe18yQVreXTWiVgIrmBUXzjnJfuba+eo4eAk6zi+/gXQkSUea8A==
mobx-react@9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-9.1.0.tgz#5e54919ca27ffad5f2c0d835148a1f681cebdbc1"
integrity sha512-DeDRTYw4AlgHw8xEXtiZdKKEnp+c5/jeUgTbTQXEqnAzfkrgYRWP3p3Nv3Whc2CEcM/mDycbDWGjxKokQdlffg==
dependencies:
mobx-react-lite "^4.0.4"
mobx@6.12.0:
version "6.12.0"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.12.0.tgz#72b2685ca5af031aaa49e77a4d76ed67fcbf9135"
integrity sha512-Mn6CN6meXEnMa0a5u6a5+RKrqRedHBhZGd15AWLk9O6uFY4KYHzImdt8JI8WODo1bjTSRnwXhJox+FCUZhCKCQ==
module-details-from-path@^1.0.3:
version "1.0.3"
@ -15686,6 +15688,11 @@ use-memo-one@^1.1.1:
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99"
integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==
use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"