Use autogenerated types for alert_receive_channels (#3851)
# What this PR does - autogenerate new types exposed by backend, remove custom types that duplicate autogenerated ones - use autogenerated types for alert receive channels - in alert_receive_channel model: - use autogenerate http client (`onCallApi`) for http requests - extract methods that don't update state into alert_receive_channel.helpers.ts and make them pure (they accept AlertReceiveChannelStore as param) to avoid inconsistency and issues with `this` binding - use `makeAutoObservable` - remove unneeded decorators - rename update* methods to fetch* whenever such methods retrieve data from backend with GET requests - in other models use `@action.bound` for actions and arrow functions for store methods that are not actions (in subsequent PRs we will apply the same changes as in alert_receive_channel, this is just for now until we do it) - refactor http-client so that it shows global notification on http errors automatically and provide the possibility to opt-out from it when making a call - improve type-safety of `GSelect` - fix bug related to attaching alert group (https://raintank-corp.slack.com/archives/C04JCU51NF8/p1707476487580579) ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/3331 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --------- Co-authored-by: Vadim Stepanov <vadimkerr@gmail.com>
This commit is contained in:
parent
8f02d8513c
commit
6da36b3c0b
84 changed files with 3914 additions and 1247 deletions
|
|
@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Changed
|
||||
|
||||
- Check for permissions on Slack escalate command ([#3891](https://github.com/grafana/oncall/pull/3891))
|
||||
- Use autogenerated types on the frontend for alert receive channels ([#3331](https://github.com/grafana/oncall/issues/3331))
|
||||
- Update OnCall Insights dashboard @Ferril ([#3875](https://github.com/grafana/oncall/pull/3875))
|
||||
- Do not delete webhook if its team is deleted @mderynck ([#3873](https://github.com/grafana/oncall/pull/3873))
|
||||
- Update user details internal API perms ([#3900](https://github.com/grafana/oncall/pull/3900))
|
||||
|
|
|
|||
|
|
@ -518,11 +518,11 @@ In order to automate types creation and prevent API usage pitfalls, OnCall proje
|
|||
|
||||
```ts
|
||||
import { ApiSchemas } from "network/oncall-api/api.types";
|
||||
import onCallApi from "network/oncall-api/http-client";
|
||||
import { onCallApi } from "network/oncall-api/http-client";
|
||||
|
||||
const {
|
||||
data: { results },
|
||||
} = await onCallApi.GET("/alertgroups/");
|
||||
} = await onCallApi().GET("/alertgroups/");
|
||||
const alertGroups: Array<ApiSchemas["AlertGroup"]> = results;
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ class AlertReceiveChannelSerializer(
|
|||
demo_alert_payload = serializers.JSONField(source="config.example_payload", read_only=True)
|
||||
routes_count = serializers.SerializerMethodField()
|
||||
connected_escalations_chains_count = serializers.SerializerMethodField()
|
||||
inbound_email = serializers.CharField(required=False)
|
||||
inbound_email = serializers.CharField(required=False, read_only=True)
|
||||
is_legacy = serializers.SerializerMethodField()
|
||||
alert_group_labels = IntegrationAlertGroupLabelsSerializer(source="*", required=False)
|
||||
|
||||
|
|
|
|||
|
|
@ -511,7 +511,11 @@ class AlertReceiveChannelView(
|
|||
fields={
|
||||
"uid": serializers.CharField(),
|
||||
"name": serializers.CharField(),
|
||||
"contact_points": serializers.ListField(child=serializers.CharField()),
|
||||
"contact_points": inline_serializer(
|
||||
"AlertReceiveChannelConnectedContactPointsInner",
|
||||
fields={"name": serializers.CharField(), "notification_connected": serializers.BooleanField()},
|
||||
many=True,
|
||||
),
|
||||
},
|
||||
many=True,
|
||||
)
|
||||
|
|
|
|||
2
grafana-plugin/.gitignore
vendored
2
grafana-plugin/.gitignore
vendored
|
|
@ -15,6 +15,6 @@ yarn-error.log*
|
|||
grafana-plugin.yml
|
||||
|
||||
# playwright
|
||||
/playwright-report/
|
||||
/playwright-report*
|
||||
/playwright/.cache/
|
||||
/e2e-tests/storageState.json
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@
|
|||
"ci-report": "grafana-toolkit plugin:ci-report",
|
||||
"start": "yarn watch",
|
||||
"plop": "plop",
|
||||
"setversion": "setversion"
|
||||
"setversion": "setversion",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ import { IntegrationBlock } from 'components/Integrations/IntegrationBlock';
|
|||
import { Tag } from 'components/Tag/Tag';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { WithConfirm } from 'components/WithConfirm/WithConfirm';
|
||||
import { AlertReceiveChannel, ContactPoint } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ContactPoint } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import styles from 'pages/integration/Integration.module.scss';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { getVar } from 'utils/DOM';
|
||||
|
|
@ -46,7 +48,7 @@ interface IntegrationContactPointState {
|
|||
}
|
||||
|
||||
export const IntegrationContactPoint: React.FC<{
|
||||
id: AlertReceiveChannel['id'];
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
}> = observer(({ id }) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const contactPoints = alertReceiveChannelStore.connectedContactPoints[id];
|
||||
|
|
@ -84,7 +86,7 @@ export const IntegrationContactPoint: React.FC<{
|
|||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const response = await alertReceiveChannelStore.getGrafanaAlertingContactPoints();
|
||||
const response = await AlertReceiveChannelHelper.getGrafanaAlertingContactPoints();
|
||||
setState({
|
||||
allContactPoints: response,
|
||||
dataSourceOptions: response.map((res) => ({ label: res.name, value: res.uid })),
|
||||
|
|
@ -281,12 +283,11 @@ export const IntegrationContactPoint: React.FC<{
|
|||
aria-label="Disconnect Contact Point"
|
||||
name="trash-alt"
|
||||
onClick={() => {
|
||||
alertReceiveChannelStore
|
||||
.disconnectContactPoint(id, item.dataSourceId, item.contactPoint)
|
||||
AlertReceiveChannelHelper.disconnectContactPoint(id, item.dataSourceId, item.contactPoint)
|
||||
.then(() => {
|
||||
closeDrawer();
|
||||
openNotification('Contact point has been removed');
|
||||
alertReceiveChannelStore.updateConnectedContactPoints(id);
|
||||
alertReceiveChannelStore.fetchConnectedContactPoints(id);
|
||||
})
|
||||
.catch(() => openErrorNotification('An error has occurred. Please try again.'));
|
||||
}}
|
||||
|
|
@ -338,13 +339,13 @@ export const IntegrationContactPoint: React.FC<{
|
|||
setState({ isLoading: true });
|
||||
|
||||
(isExistingContactPoint
|
||||
? alertReceiveChannelStore.connectContactPoint(id, selectedAlertManager, selectedContactPoint)
|
||||
: alertReceiveChannelStore.createContactPoint(id, selectedAlertManager, selectedContactPoint)
|
||||
? AlertReceiveChannelHelper.connectContactPoint(id, selectedAlertManager, selectedContactPoint)
|
||||
: AlertReceiveChannelHelper.createContactPoint(id, selectedAlertManager, selectedContactPoint)
|
||||
)
|
||||
.then(() => {
|
||||
closeDrawer();
|
||||
openNotification('A new contact point has been connected to your integration');
|
||||
alertReceiveChannelStore.updateConnectedContactPoints(id);
|
||||
alertReceiveChannelStore.fetchConnectedContactPoints(id);
|
||||
})
|
||||
.catch((ex) => {
|
||||
const error = ex.response?.data?.detail ?? 'An error has occurred. Please try again.';
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ import { IntegrationInputField } from 'components/IntegrationInputField/Integrat
|
|||
import { IntegrationBlock } from 'components/Integrations/IntegrationBlock';
|
||||
import { Tag } from 'components/Tag/Tag';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import styles from 'pages/integration/Integration.module.scss';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { getVar } from 'utils/DOM';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
export const IntegrationHowToConnect: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id }) => {
|
||||
export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveChannel']['id'] }> = ({ id }) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id];
|
||||
const hasAlerts = !!alertReceiveChannelCounter?.alerts_count;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import { MonacoEditor, MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEdi
|
|||
import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
import { PluginLink } from 'components/PluginLink/PluginLink';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import styles from 'pages/integration/Integration.module.scss';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openNotification } from 'utils/utils';
|
||||
|
|
@ -19,7 +20,7 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface IntegrationSendDemoPayloadModalProps {
|
||||
isOpen: boolean;
|
||||
alertReceiveChannel: AlertReceiveChannel;
|
||||
alertReceiveChannel: ApiSchemas['AlertReceiveChannel'];
|
||||
onHideOrCancel: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +89,7 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
|||
<CopyToClipboard text={getCurlText()} onCopy={() => openNotification('CURL has been copied')}>
|
||||
<Button variant={'secondary'}>Copy as CURL</Button>
|
||||
</CopyToClipboard>
|
||||
<Button variant={'primary'} onClick={sendDemoAlert} data-testid="submit-send-alert">
|
||||
<Button variant={'primary'} onClick={onSendAlert} data-testid="submit-send-alert">
|
||||
Send Alert
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
|
|
@ -100,14 +101,14 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
|||
setDemoPayload(value);
|
||||
}
|
||||
|
||||
function sendDemoAlert() {
|
||||
function onSendAlert() {
|
||||
let parsedPayload = undefined;
|
||||
try {
|
||||
parsedPayload = JSON.parse(demoPayload);
|
||||
} catch (ex) {}
|
||||
|
||||
alertReceiveChannelStore.sendDemoAlert(alertReceiveChannel.id, parsedPayload).then(() => {
|
||||
alertReceiveChannelStore.updateCounters();
|
||||
AlertReceiveChannelHelper.sendDemoAlert(alertReceiveChannel.id, parsedPayload).then(() => {
|
||||
alertReceiveChannelStore.fetchCounters();
|
||||
openNotification(<DemoNotification />);
|
||||
onHideOrCancel();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ import {
|
|||
EscalationPolicy as EscalationPolicyType,
|
||||
EscalationPolicyOption,
|
||||
} from 'models/escalation_policy/escalation_policy.types';
|
||||
import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
|
||||
import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook';
|
||||
import { ScheduleStore } from 'models/schedule/schedule';
|
||||
import { SelectOption } from 'state/types';
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { Schedule } from 'models/schedule/schedule.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { UserGroup } from 'models/user_group/user_group.types';
|
||||
import { SelectOption, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { getVar } from 'utils/DOM';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
|
||||
|
|
@ -34,7 +36,7 @@ import styles from './EscalationPolicy.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface ElementSortableProps {
|
||||
interface ElementSortableProps extends WithStoreProps {
|
||||
index: number;
|
||||
}
|
||||
|
||||
|
|
@ -51,9 +53,6 @@ export interface EscalationPolicyProps extends ElementSortableProps {
|
|||
backgroundClassName?: string;
|
||||
backgroundHexNumber?: string;
|
||||
isSlackInstalled: boolean;
|
||||
teamStore: GrafanaTeamStore;
|
||||
outgoingWebhookStore: OutgoingWebhookStore;
|
||||
scheduleStore: ScheduleStore;
|
||||
}
|
||||
|
||||
class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
||||
|
|
@ -81,13 +80,13 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
)}
|
||||
{escalationOption &&
|
||||
reactStringReplace(escalationOption.display_name, /\{\{([^}]+)\}\}/g, this.replacePlaceholder)}
|
||||
{this._renderNote()}
|
||||
{this.renderNote()}
|
||||
{is_final || isDisabled ? null : (
|
||||
<WithPermissionControlTooltip className={cx('delete')} userAction={UserActions.EscalationChainsWrite}>
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
className={cx('delete', 'control')}
|
||||
onClick={this._handleDelete}
|
||||
onClick={this.handleDelete}
|
||||
size="sm"
|
||||
tooltip="Delete"
|
||||
tooltipPlacement="top"
|
||||
|
|
@ -105,15 +104,15 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
case 'timerange':
|
||||
return this.renderTimeRange();
|
||||
case 'users':
|
||||
return this._renderNotifyToUsersQueue();
|
||||
return this.renderNotifyToUsersQueue();
|
||||
case 'wait_delay':
|
||||
return this._renderWaitDelays();
|
||||
return this.renderWaitDelays();
|
||||
case 'slack_user_group':
|
||||
return this._renderNotifyUserGroup();
|
||||
return this.renderNotifyUserGroup();
|
||||
case 'schedule':
|
||||
return this._renderNotifySchedule();
|
||||
return this.renderNotifySchedule();
|
||||
case 'custom_webhook':
|
||||
return this._renderTriggerCustomWebhook();
|
||||
return this.renderTriggerCustomWebhook();
|
||||
case 'num_alerts_in_window':
|
||||
return this.renderNumAlertsInWindow();
|
||||
case 'num_minutes_in_window':
|
||||
|
|
@ -124,7 +123,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
}
|
||||
};
|
||||
|
||||
_renderNote() {
|
||||
renderNote() {
|
||||
const { data, isSlackInstalled, escalationChoices } = this.props;
|
||||
const { step } = data;
|
||||
|
||||
|
|
@ -152,32 +151,39 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
}
|
||||
}
|
||||
|
||||
private _renderNotifyToUsersQueue() {
|
||||
const { data, isDisabled } = this.props;
|
||||
renderNotifyToUsersQueue() {
|
||||
const {
|
||||
data,
|
||||
isDisabled,
|
||||
store: { userStore },
|
||||
} = this.props;
|
||||
const { notify_to_users_queue } = data;
|
||||
|
||||
return (
|
||||
<WithPermissionControlTooltip key="users-multiple" userAction={UserActions.EscalationChainsWrite}>
|
||||
<GSelect
|
||||
<GSelect<User>
|
||||
isMulti
|
||||
showSearch
|
||||
allowClear
|
||||
disabled={isDisabled}
|
||||
modelName="userStore"
|
||||
displayField="username"
|
||||
valueField="pk"
|
||||
placeholder="Select Users"
|
||||
className={cx('select', 'control', 'multiSelect')}
|
||||
value={notify_to_users_queue}
|
||||
onChange={this._getOnChangeHandler('notify_to_users_queue')}
|
||||
onChange={this.getOnChangeHandler('notify_to_users_queue')}
|
||||
getOptionLabel={({ value }: SelectableValue) => <UserTooltip id={value} />}
|
||||
width={'auto'}
|
||||
items={userStore.items}
|
||||
fetchItemsFn={userStore.updateItems}
|
||||
fetchItemFn={userStore.updateItem}
|
||||
getSearchResult={userStore.getSearchResult}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
private renderImportance() {
|
||||
renderImportance() {
|
||||
const { data, isDisabled } = this.props;
|
||||
const { important } = data;
|
||||
|
||||
|
|
@ -189,7 +195,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
disabled={isDisabled}
|
||||
value={Number(important)}
|
||||
// @ts-ignore
|
||||
onChange={this._getOnSelectChangeHandler('important')}
|
||||
onChange={this.getOnSelectChangeHandler('important')}
|
||||
options={[
|
||||
{
|
||||
value: 0,
|
||||
|
|
@ -222,7 +228,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
);
|
||||
}
|
||||
|
||||
private renderTimeRange() {
|
||||
renderTimeRange() {
|
||||
const { data, isDisabled } = this.props;
|
||||
|
||||
return (
|
||||
|
|
@ -231,14 +237,14 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
from={data.from_time}
|
||||
to={data.to_time}
|
||||
disabled={isDisabled}
|
||||
onChange={this._getOnTimeRangeChangeHandler()}
|
||||
onChange={this.getOnTimeRangeChangeHandler()}
|
||||
className={cx('select', 'control')}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
private _renderWaitDelays() {
|
||||
renderWaitDelays() {
|
||||
const { data, isDisabled, waitDelays = [] } = this.props;
|
||||
const { wait_delay } = data;
|
||||
|
||||
|
|
@ -251,7 +257,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
className={cx('select', 'control')}
|
||||
// @ts-ignore
|
||||
value={wait_delay}
|
||||
onChange={this._getOnSelectChangeHandler('wait_delay')}
|
||||
onChange={this.getOnSelectChangeHandler('wait_delay')}
|
||||
options={waitDelays.map((waitDelay: SelectOption) => ({
|
||||
value: waitDelay.value,
|
||||
label: waitDelay.display_name,
|
||||
|
|
@ -262,7 +268,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
);
|
||||
}
|
||||
|
||||
private renderNumAlertsInWindow() {
|
||||
renderNumAlertsInWindow() {
|
||||
const { data, isDisabled } = this.props;
|
||||
const { num_alerts_in_window } = data;
|
||||
|
||||
|
|
@ -273,7 +279,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
disabled={isDisabled}
|
||||
className={cx('control')}
|
||||
value={num_alerts_in_window}
|
||||
onChange={this._getOnInputChangeHandler('num_alerts_in_window')}
|
||||
onChange={this.getOnInputChangeHandler('num_alerts_in_window')}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
node.setAttribute('type', 'number');
|
||||
|
|
@ -285,7 +291,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
);
|
||||
}
|
||||
|
||||
private renderNumMinutesInWindowOptions() {
|
||||
renderNumMinutesInWindowOptions() {
|
||||
const { data, isDisabled, numMinutesInWindowOptions = [] } = this.props;
|
||||
const { num_minutes_in_window } = data;
|
||||
|
||||
|
|
@ -298,7 +304,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
className={cx('select', 'control')}
|
||||
// @ts-ignore
|
||||
value={num_minutes_in_window}
|
||||
onChange={this._getOnSelectChangeHandler('num_minutes_in_window')}
|
||||
onChange={this.getOnSelectChangeHandler('num_minutes_in_window')}
|
||||
options={numMinutesInWindowOptions.map((waitDelay: SelectOption) => ({
|
||||
value: waitDelay.value,
|
||||
label: waitDelay.display_name,
|
||||
|
|
@ -308,25 +314,31 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
);
|
||||
}
|
||||
|
||||
private _renderNotifySchedule() {
|
||||
const { data, isDisabled, teamStore, scheduleStore } = this.props;
|
||||
renderNotifySchedule() {
|
||||
const {
|
||||
data,
|
||||
isDisabled,
|
||||
store: { grafanaTeamStore, scheduleStore },
|
||||
} = this.props;
|
||||
const { notify_schedule } = data;
|
||||
|
||||
return (
|
||||
<WithPermissionControlTooltip key="notify_schedule" userAction={UserActions.EscalationChainsWrite}>
|
||||
<GSelect
|
||||
<GSelect<Schedule>
|
||||
showSearch
|
||||
allowClear
|
||||
disabled={isDisabled}
|
||||
modelName="scheduleStore"
|
||||
items={scheduleStore.items}
|
||||
fetchItemsFn={scheduleStore.updateItems}
|
||||
getSearchResult={scheduleStore.getSearchResult}
|
||||
displayField="name"
|
||||
valueField="id"
|
||||
placeholder="Select Schedule"
|
||||
className={cx('select', 'control')}
|
||||
value={notify_schedule}
|
||||
onChange={this._getOnChangeHandler('notify_schedule')}
|
||||
onChange={this.getOnChangeHandler('notify_schedule')}
|
||||
getOptionLabel={(item: SelectableValue) => {
|
||||
const team = teamStore.items[scheduleStore.items[item.value].team];
|
||||
const team = grafanaTeamStore.items[scheduleStore.items[item.value].team];
|
||||
return (
|
||||
<>
|
||||
<Text>{item.label} </Text>
|
||||
|
|
@ -339,45 +351,58 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
);
|
||||
}
|
||||
|
||||
private _renderNotifyUserGroup() {
|
||||
const { data, isDisabled } = this.props;
|
||||
renderNotifyUserGroup() {
|
||||
const {
|
||||
data,
|
||||
isDisabled,
|
||||
store: { userGroupStore },
|
||||
} = this.props;
|
||||
const { notify_to_group } = data;
|
||||
|
||||
return (
|
||||
<WithPermissionControlTooltip key="notify_to_group" userAction={UserActions.EscalationChainsWrite}>
|
||||
<GSelect
|
||||
<GSelect<UserGroup[]>
|
||||
disabled={isDisabled}
|
||||
modelName="userGroupStore"
|
||||
items={userGroupStore.items}
|
||||
fetchItemsFn={userGroupStore.updateItems}
|
||||
getSearchResult={userGroupStore.getSearchResult}
|
||||
displayField="name"
|
||||
valueField="id"
|
||||
placeholder="Select User Group"
|
||||
className={cx('select', 'control')}
|
||||
value={notify_to_group}
|
||||
onChange={this._getOnChangeHandler('notify_to_group')}
|
||||
onChange={this.getOnChangeHandler('notify_to_group')}
|
||||
width={'auto'}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
private _renderTriggerCustomWebhook() {
|
||||
const { data, isDisabled, teamStore, outgoingWebhookStore } = this.props;
|
||||
renderTriggerCustomWebhook() {
|
||||
const {
|
||||
data,
|
||||
isDisabled,
|
||||
store: { grafanaTeamStore, outgoingWebhookStore },
|
||||
} = this.props;
|
||||
const { custom_webhook } = data;
|
||||
|
||||
return (
|
||||
<WithPermissionControlTooltip key="custom-webhook" userAction={UserActions.EscalationChainsWrite}>
|
||||
<GSelect
|
||||
<GSelect<OutgoingWebhook>
|
||||
showSearch
|
||||
disabled={isDisabled}
|
||||
modelName="outgoingWebhookStore"
|
||||
items={outgoingWebhookStore.items}
|
||||
fetchItemsFn={outgoingWebhookStore.updateItems}
|
||||
fetchItemFn={outgoingWebhookStore.updateItem}
|
||||
getSearchResult={outgoingWebhookStore.getSearchResult}
|
||||
displayField="name"
|
||||
valueField="id"
|
||||
placeholder="Select Webhook"
|
||||
className={cx('select', 'control')}
|
||||
value={custom_webhook}
|
||||
onChange={this._getOnChangeHandler('custom_webhook')}
|
||||
onChange={this.getOnChangeHandler('custom_webhook')}
|
||||
getOptionLabel={(item: SelectableValue) => {
|
||||
const team = teamStore.items[outgoingWebhookStore.items[item.value].team];
|
||||
const team = grafanaTeamStore.items[outgoingWebhookStore.items[item.value].team];
|
||||
return (
|
||||
<>
|
||||
<Text>{item.label} </Text>
|
||||
|
|
@ -395,7 +420,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
);
|
||||
}
|
||||
|
||||
_getOnSelectChangeHandler = (field: string) => {
|
||||
getOnSelectChangeHandler = (field: string) => {
|
||||
return (option: SelectableValue) => {
|
||||
const { data, onChange = () => {} } = this.props;
|
||||
const { id } = data;
|
||||
|
|
@ -409,7 +434,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
};
|
||||
};
|
||||
|
||||
_getOnInputChangeHandler = (field: string) => {
|
||||
getOnInputChangeHandler = (field: string) => {
|
||||
const { data, onChange = () => {} } = this.props;
|
||||
const { id } = data;
|
||||
|
||||
|
|
@ -423,7 +448,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
};
|
||||
};
|
||||
|
||||
_getOnChangeHandler = (field: string) => {
|
||||
getOnChangeHandler = (field: string) => {
|
||||
return (value: any) => {
|
||||
const { data, onChange = () => {} } = this.props;
|
||||
const { id } = data;
|
||||
|
|
@ -437,7 +462,7 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
};
|
||||
};
|
||||
|
||||
_getOnTimeRangeChangeHandler() {
|
||||
getOnTimeRangeChangeHandler() {
|
||||
return (value: string[]) => {
|
||||
const { data, onChange = () => {} } = this.props;
|
||||
const { id } = data;
|
||||
|
|
@ -452,11 +477,13 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
|||
};
|
||||
}
|
||||
|
||||
_handleDelete = () => {
|
||||
handleDelete = () => {
|
||||
const { onDelete, data } = this.props;
|
||||
|
||||
onDelete(data);
|
||||
};
|
||||
}
|
||||
|
||||
export const EscalationPolicy = SortableElement(_EscalationPolicy) as React.ComponentClass<EscalationPolicyProps>;
|
||||
export const EscalationPolicy = withMobXProviderContext(
|
||||
SortableElement(_EscalationPolicy) as React.ComponentClass<EscalationPolicyProps>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import { PluginLink } from 'components/PluginLink/PluginLink';
|
|||
import { Text } from 'components/Text/Text';
|
||||
import { TeamName } from 'containers/TeamName/TeamName';
|
||||
import { HeartGreenIcon, HeartRedIcon } from 'icons/Icons';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './AlertReceiveChannelCard.module.scss';
|
||||
|
|
@ -19,7 +20,7 @@ import styles from './AlertReceiveChannelCard.module.scss';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface AlertReceiveChannelCardProps {
|
||||
id: AlertReceiveChannel['id'];
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
onShowHeartbeatModal: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ export const AlertReceiveChannelCard = observer((props: AlertReceiveChannelCardP
|
|||
|
||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
|
||||
const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel);
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const MSTeamsConnector = (props: MSTeamsConnectorProps) => {
|
|||
const { channelFilterId } = props;
|
||||
|
||||
const store = useStore();
|
||||
const { alertReceiveChannelStore } = store;
|
||||
const { alertReceiveChannelStore, msteamsChannelStore } = store;
|
||||
|
||||
const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId];
|
||||
|
||||
|
|
@ -54,11 +54,13 @@ export const MSTeamsConnector = (props: MSTeamsConnectorProps) => {
|
|||
</div>
|
||||
Post to Microsoft Teams channel
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<GSelect
|
||||
<GSelect<MSTeamsChannel>
|
||||
showSearch
|
||||
allowClear
|
||||
className={cx('select', 'control')}
|
||||
modelName="msteamsChannelStore"
|
||||
items={msteamsChannelStore.items}
|
||||
fetchItemsFn={msteamsChannelStore.updateItems}
|
||||
getSearchResult={msteamsChannelStore.getSearchResult}
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Microsoft Teams Channel"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export const SlackConnector = (props: SlackConnectorProps) => {
|
|||
const {
|
||||
organizationStore: { currentOrganization },
|
||||
alertReceiveChannelStore,
|
||||
slackChannelStore,
|
||||
} = store;
|
||||
|
||||
const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId];
|
||||
|
|
@ -56,11 +57,14 @@ export const SlackConnector = (props: SlackConnectorProps) => {
|
|||
</div>
|
||||
Slack Channel
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<GSelect
|
||||
<GSelect<SlackChannel>
|
||||
showSearch
|
||||
allowClear
|
||||
className={cx('select', 'control')}
|
||||
modelName="slackChannelStore"
|
||||
items={slackChannelStore.items}
|
||||
fetchItemsFn={slackChannelStore.updateItems}
|
||||
fetchItemFn={slackChannelStore.updateItem}
|
||||
getSearchResult={slackChannelStore.getSearchResult}
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Slack Channel"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ interface TelegramConnectorProps {
|
|||
|
||||
export const TelegramConnector = ({ channelFilterId }: TelegramConnectorProps) => {
|
||||
const store = useStore();
|
||||
const { alertReceiveChannelStore } = store;
|
||||
const { alertReceiveChannelStore, telegramChannelStore } = store;
|
||||
|
||||
const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId];
|
||||
|
||||
|
|
@ -46,11 +46,13 @@ export const TelegramConnector = ({ channelFilterId }: TelegramConnectorProps) =
|
|||
</div>
|
||||
Post to telegram channel
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<GSelect
|
||||
<GSelect<TelegramChannel>
|
||||
showSearch
|
||||
allowClear
|
||||
className={cx('select', 'control')}
|
||||
modelName="telegramChannelStore"
|
||||
items={telegramChannelStore.items}
|
||||
fetchItemsFn={telegramChannelStore.updateItems}
|
||||
getSearchResult={telegramChannelStore.getSearchResult}
|
||||
displayField="channel_name"
|
||||
valueField="id"
|
||||
placeholder="Select Telegram Channel"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,17 @@ interface GroupedAlertNumberProps {
|
|||
value: Alert['pk'];
|
||||
}
|
||||
|
||||
const GroupedAlertNumber = observer(({ value }: GroupedAlertNumberProps) => {
|
||||
const { alertGroupStore } = useStore();
|
||||
const alert = alertGroupStore.items[value];
|
||||
|
||||
return (
|
||||
<div>
|
||||
#{alert?.inside_organization_number} {alert?.render_for_web?.title}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachIncidentFormProps) => {
|
||||
const store = useStore();
|
||||
|
||||
|
|
@ -45,17 +56,6 @@ export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachInci
|
|||
});
|
||||
}, [selected, alertGroupStore, id, onHide, onUpdate]);
|
||||
|
||||
const GroupedAlertNumber = observer(({ value }: GroupedAlertNumberProps) => {
|
||||
const { alertGroupStore } = useStore();
|
||||
const alert = alertGroupStore.items[value];
|
||||
|
||||
return (
|
||||
<div>
|
||||
#{alert?.inside_organization_number} {alert?.render_for_web?.title}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
|
|
@ -74,12 +74,15 @@ export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachInci
|
|||
description="Linking alert groups together can help the team investigate the underlying issue."
|
||||
>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<GSelect
|
||||
<GSelect<Alert>
|
||||
showSearch
|
||||
modelName="alertGroupStore"
|
||||
items={alertGroupStore.items}
|
||||
fetchItemsFn={alertGroupStore.fetchItemsAvailableForAttachment}
|
||||
fetchItemFn={alertGroupStore.updateItem}
|
||||
getSearchResult={alertGroupStore.getSearchResult}
|
||||
valueField="pk"
|
||||
displayField="render_for_web.title"
|
||||
placeholder="Select Incident"
|
||||
placeholder="Select Alert Group"
|
||||
className={cx('select', 'control')}
|
||||
filterOptions={(optionId) => optionId !== id}
|
||||
value={selected}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import { Block } from 'components/GBlock/Block';
|
|||
import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { IncidentMatcher } from 'containers/IncidentMatcher/IncidentMatcher';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ChannelFilter, FilteringTermType } from 'models/channel_filter/channel_filter.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openErrorNotification } from 'utils/utils';
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface ChannelFilterFormProps {
|
||||
id: ChannelFilter['id'] | 'new';
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
onHide: () => void;
|
||||
onUpdate: (channelFilterId: ChannelFilter['id']) => void;
|
||||
data?: ChannelFilter;
|
||||
|
|
@ -63,7 +64,7 @@ export const ChannelFilterForm = observer((props: ChannelFilterFormProps) => {
|
|||
|
||||
const onUpdateClickCallback = useCallback(() => {
|
||||
(id === 'new'
|
||||
? alertReceiveChannelStore.createChannelFilter({
|
||||
? AlertReceiveChannelHelper.createChannelFilter({
|
||||
alert_receive_channel: alertReceiveChannelId,
|
||||
filtering_term: filteringTerm,
|
||||
filtering_term_type: filteringTermType,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesF
|
|||
import { Block } from 'components/GBlock/Block';
|
||||
import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openErrorNotification } from 'utils/utils';
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ const cx = cn.bind(styles);
|
|||
interface EditRegexpRouteTemplateModalProps {
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
template?: TemplateForEdit;
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'];
|
||||
alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
onHide: () => void;
|
||||
onUpdateRoute: (values: any, channelFilterId: ChannelFilter['id'], type: number) => void;
|
||||
onOpenEditIntegrationTemplate?: (templateName: string, channelFilterId: ChannelFilter['id']) => void;
|
||||
|
|
@ -59,14 +60,14 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
|
|||
}, [regexpTemplateBody]);
|
||||
|
||||
const handleConvertToJinja2 = useCallback(() => {
|
||||
alertReceiveChannelStore.convertRegexpTemplateToJinja2Template(channelFilterId).then((response) => {
|
||||
AlertReceiveChannelHelper.convertRegexpTemplateToJinja2Template(channelFilterId).then((response) => {
|
||||
alertReceiveChannelStore
|
||||
.saveChannelFilter(channelFilterId, {
|
||||
filtering_term: response?.filtering_term_as_jinja2,
|
||||
filtering_term_type: 1,
|
||||
})
|
||||
.then(() => {
|
||||
alertReceiveChannelStore.updateChannelFilters(alertReceiveChannelId, true).then(() => {
|
||||
alertReceiveChannelStore.fetchChannelFilters(alertReceiveChannelId, true).then(() => {
|
||||
onOpenEditIntegrationTemplate('route_template', channelFilterId);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const EscalationChainForm: FC<EscalationChainFormProps> = (props) => {
|
|||
const { escalationChainId, onHide, onSubmit: onSubmitProp, mode } = props;
|
||||
|
||||
const store = useStore();
|
||||
const { escalationChainStore, userStore } = store;
|
||||
const { escalationChainStore, userStore, grafanaTeamStore } = store;
|
||||
|
||||
const user = userStore.currentUser;
|
||||
|
||||
|
|
@ -92,8 +92,10 @@ export const EscalationChainForm: FC<EscalationChainFormProps> = (props) => {
|
|||
<Modal isOpen title={`${mode} Escalation Chain`} onDismiss={onHide}>
|
||||
<div className={cx('root')}>
|
||||
<Field label="Assign to team">
|
||||
<GSelect
|
||||
modelName="grafanaTeamStore"
|
||||
<GSelect<GrafanaTeam>
|
||||
items={grafanaTeamStore.items}
|
||||
fetchItemsFn={grafanaTeamStore.updateItems}
|
||||
getSearchResult={grafanaTeamStore.getSearchResult}
|
||||
displayField="name"
|
||||
valueField="id"
|
||||
showSearch
|
||||
|
|
|
|||
|
|
@ -6,23 +6,24 @@ import cn from 'classnames/bind';
|
|||
import { get, isNil } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { BaseStore } from 'models/base_store';
|
||||
import { RootBaseStore } from 'state/rootBaseStore/RootBaseStore';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { useDebouncedCallback } from 'utils/hooks';
|
||||
import { PropertiesThatExtendsAnotherClass } from 'utils/types';
|
||||
|
||||
import styles from './GSelect.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface GSelectProps {
|
||||
interface GSelectProps<Item> {
|
||||
items: {
|
||||
[key: string]: Item;
|
||||
};
|
||||
fetchItemsFn: (query?: string) => Promise<Item[] | void>;
|
||||
fetchItemFn?: (id: string) => Promise<Item | void>;
|
||||
getSearchResult: (query?: string) => Item[] | { page_size: number; count: number; results: Item[] };
|
||||
placeholder: string;
|
||||
isLoading?: boolean;
|
||||
value?: string | string[] | null;
|
||||
defaultValue?: string | string[] | null;
|
||||
onChange: (value: string, item: any) => void;
|
||||
modelName: PropertiesThatExtendsAnotherClass<RootBaseStore, BaseStore>;
|
||||
autoFocus?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
disabled?: boolean;
|
||||
|
|
@ -44,7 +45,7 @@ interface GSelectProps {
|
|||
icon?: string;
|
||||
}
|
||||
|
||||
export const GSelect = observer((props: GSelectProps) => {
|
||||
export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
|
||||
const {
|
||||
autoFocus,
|
||||
showSearch = false,
|
||||
|
|
@ -58,7 +59,6 @@ export const GSelect = observer((props: GSelectProps) => {
|
|||
onChange,
|
||||
disabled,
|
||||
showError,
|
||||
modelName,
|
||||
displayField = 'display_name',
|
||||
valueField = 'id',
|
||||
isMulti = false,
|
||||
|
|
@ -68,34 +68,37 @@ export const GSelect = observer((props: GSelectProps) => {
|
|||
filterOptions,
|
||||
width = null,
|
||||
icon = null,
|
||||
items: propItems,
|
||||
fetchItemsFn,
|
||||
fetchItemFn,
|
||||
getSearchResult,
|
||||
} = props;
|
||||
|
||||
const store = useStore();
|
||||
const model = (store as any)[modelName];
|
||||
|
||||
const onChangeCallback = useCallback(
|
||||
(option) => {
|
||||
if (isMulti) {
|
||||
const values = option.map((option: SelectableValue) => option.value);
|
||||
const items = option.map((option: SelectableValue) => model.items[option.value]);
|
||||
const items = option.map((option: SelectableValue) => propItems[option.value]);
|
||||
|
||||
onChange(values, items);
|
||||
} else {
|
||||
if (option) {
|
||||
const id = option.value;
|
||||
const item = model.items[id];
|
||||
const item = propItems[id];
|
||||
onChange(id, item);
|
||||
} else {
|
||||
onChange(null, null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[model, onChange]
|
||||
[propItems, onChange]
|
||||
);
|
||||
|
||||
const loadOptions = useDebouncedCallback((query: string, cb) => {
|
||||
model.updateItems(query).then(() => {
|
||||
const searchResult = model.getSearchResult(query);
|
||||
fetchItemsFn(query).then(() => {
|
||||
const searchResult = getSearchResult(query);
|
||||
// TODO: we need to unify interface of search results to get rid of ts-ignore
|
||||
// @ts-ignore
|
||||
let items = Array.isArray(searchResult.results) ? searchResult.results : searchResult;
|
||||
if (filterOptions) {
|
||||
items = items.filter((opt: any) => filterOptions(opt[valueField]));
|
||||
|
|
@ -112,19 +115,17 @@ export const GSelect = observer((props: GSelectProps) => {
|
|||
|
||||
const values = isMulti
|
||||
? (value ? (value as string[]) : [])
|
||||
.filter((id) => id in model.items)
|
||||
.filter((id) => id in propItems)
|
||||
.map((id: string) => ({
|
||||
value: id,
|
||||
label: get(model.items[id], displayField),
|
||||
description: getDescription && getDescription(model.items[id]),
|
||||
label: get(propItems[id], displayField),
|
||||
description: getDescription && getDescription(propItems[id]),
|
||||
}))
|
||||
: model.items[value as string]
|
||||
: propItems[value as string]
|
||||
? {
|
||||
value,
|
||||
label: get(model.items[value as string], displayField)
|
||||
? get(model.items[value as string], displayField)
|
||||
: 'hidden',
|
||||
description: getDescription && getDescription(model.items[value as string]),
|
||||
label: get(propItems[value as string], displayField) ? get(propItems[value as string], displayField) : 'hidden',
|
||||
description: getDescription && getDescription(propItems[value as string]),
|
||||
}
|
||||
: value;
|
||||
|
||||
|
|
@ -132,8 +133,8 @@ export const GSelect = observer((props: GSelectProps) => {
|
|||
const values = isMulti ? value : [value];
|
||||
|
||||
(values ? (values as string[]) : []).forEach((value: string) => {
|
||||
if (!isNil(value) && !model.items[value] && model.updateItem) {
|
||||
model.updateItem(value, true);
|
||||
if (!isNil(value) && !propItems[value] && fetchItemFn) {
|
||||
fetchItemFn(value);
|
||||
}
|
||||
});
|
||||
}, [value]);
|
||||
|
|
|
|||
|
|
@ -52,9 +52,11 @@ export const GrafanaTeamSelect = observer(
|
|||
}
|
||||
|
||||
const select = (
|
||||
<GSelect
|
||||
<GSelect<GrafanaTeam>
|
||||
showSearch
|
||||
modelName="grafanaTeamStore"
|
||||
items={grafanaTeamStore.items}
|
||||
fetchItemsFn={grafanaTeamStore.updateItems}
|
||||
getSearchResult={grafanaTeamStore.getSearchResult}
|
||||
displayField="name"
|
||||
valueField="id"
|
||||
placeholder="Select team"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Emoji from 'react-emoji-render';
|
|||
import { Text } from 'components/Text/Text';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { HeartGreenIcon, HeartRedIcon } from 'icons/Icons';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { SelectOption } from 'state/types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
|
@ -20,7 +20,7 @@ import styles from './HeartbeatForm.module.css';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface HeartBeatModalProps {
|
||||
alertReceveChannelId: AlertReceiveChannel['id'];
|
||||
alertReceveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import { Text } from 'components/Text/Text';
|
|||
import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
|
||||
import styles from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss';
|
||||
import { RouteButtonsDisplay } from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper';
|
||||
import { IntegrationHelper } from 'pages/integration/Integration.helper';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -19,7 +19,7 @@ import { useStore } from 'state/useStore';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface CollapsedIntegrationRouteDisplayProps {
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
routeIndex: number;
|
||||
toggle: () => void;
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ import { EscalationChainSteps } from 'containers/EscalationChainSteps/Escalation
|
|||
import styles from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss';
|
||||
import { TeamName } from 'containers/TeamName/TeamName';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper';
|
||||
import { IntegrationHelper } from 'pages/integration/Integration.helper';
|
||||
import { MONACO_INPUT_HEIGHT_SMALL } from 'pages/integration/IntegrationCommon.config';
|
||||
|
|
@ -47,7 +47,7 @@ import { openNotification } from 'utils/utils';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface ExpandedIntegrationRouteDisplayProps {
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
routeIndex: number;
|
||||
templates: AlertTemplatesDTO[];
|
||||
|
|
@ -372,7 +372,7 @@ const ReadOnlyEscalationChain: React.FC<{ escalationChainId: string }> = ({ esca
|
|||
};
|
||||
|
||||
interface RouteButtonsDisplayProps {
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
routeIndex: number;
|
||||
setRouteIdForDeletion(): void;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { observer } from 'mobx-react';
|
|||
import { IntegrationInputField } from 'components/IntegrationInputField/IntegrationInputField';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { SelectOption } from 'state/types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
|
@ -20,7 +20,7 @@ import styles from './IntegrationHeartbeatForm.module.scss';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface IntegrationHeartbeatFormProps {
|
||||
alertReceveChannelId: AlertReceiveChannel['id'];
|
||||
alertReceveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I
|
|||
|
||||
openNotification('Heartbeat settings have been updated');
|
||||
|
||||
await alertReceiveChannelStore.loadItem(alertReceveChannelId);
|
||||
await alertReceiveChannelStore.fetchItemById(alertReceveChannelId);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor';
|
|||
import { MONACO_READONLY_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { getTemplatesToRender } from 'containers/IntegrationContainers/IntegrationTemplatesList.config';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { IntegrationHelper } from 'pages/integration/Integration.helper';
|
||||
import styles from 'pages/integration/Integration.module.scss';
|
||||
import { MONACO_INPUT_HEIGHT_TALL } from 'pages/integration/IntegrationCommon.config';
|
||||
|
|
@ -22,7 +22,7 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface IntegrationTemplateListProps {
|
||||
templates: AlertTemplatesDTO[];
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
openEditTemplateModal: (templateName: string | string[]) => void;
|
||||
alertReceiveChannelIsBasedOnAlertManager: boolean;
|
||||
alertReceiveChannelAllowSourceBasedResolving: boolean;
|
||||
|
|
@ -37,9 +37,9 @@ export const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = o
|
|||
alertReceiveChannelAllowSourceBasedResolving,
|
||||
}) => {
|
||||
const { alertReceiveChannelStore, features } = useStore();
|
||||
const [isRestoringTemplate, setIsRestoringTemplate] = useState<boolean>(false);
|
||||
const [isRestoringTemplate, setIsRestoringTemplate] = useState(false);
|
||||
const [templateRestoreName, setTemplateRestoreName] = useState<string>(undefined);
|
||||
const [autoresolveValue, setAutoresolveValue] = useState<boolean>(alertReceiveChannelAllowSourceBasedResolving);
|
||||
const [autoresolveValue, setAutoresolveValue] = useState(alertReceiveChannelAllowSourceBasedResolving);
|
||||
|
||||
const handleSaveClick = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAutoresolveValue(event.target.checked);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ import React from 'react';
|
|||
|
||||
import { Icon, Label, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
import { FormItemType } from 'components/GForm/GForm.types';
|
||||
import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
|
||||
import { generateAssignToTeamInputDescription } from 'utils/consts';
|
||||
|
||||
export const form: { name: string; fields: FormItem[] } = {
|
||||
export const getForm = (grafanaTeamStore: GrafanaTeamStore) => ({
|
||||
name: 'Integration',
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -33,7 +34,9 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
),
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'grafanaTeamStore',
|
||||
items: grafanaTeamStore.items,
|
||||
fetchItemsFn: grafanaTeamStore.updateItems,
|
||||
getSearchResult: grafanaTeamStore.getSearchResult,
|
||||
displayField: 'name',
|
||||
valueField: 'id',
|
||||
showSearch: true,
|
||||
|
|
@ -77,4 +80,4 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
render: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
||||
export function prepareForEdit(item: AlertReceiveChannel) {
|
||||
export function prepareForEdit(item: ApiSchemas['AlertReceiveChannel']) {
|
||||
return {
|
||||
verbal_name: item.verbal_name,
|
||||
description_short: item.description_short,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, ChangeEvent, useEffect, useReducer, useRef } from 'react';
|
||||
import React, { useState, ChangeEvent, useEffect, useReducer, useRef, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
|
|
@ -26,10 +26,8 @@ import { PluginLink } from 'components/PluginLink/PluginLink';
|
|||
import { Text } from 'components/Text/Text';
|
||||
import { Labels } from 'containers/Labels/Labels';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import {
|
||||
AlertReceiveChannel,
|
||||
AlertReceiveChannelOption,
|
||||
} from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { IntegrationHelper } from 'pages/integration/Integration.helper';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -37,18 +35,18 @@ import { UserActions } from 'utils/authorization/authorization';
|
|||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
import { openErrorNotification } from 'utils/utils';
|
||||
|
||||
import { form } from './IntegrationForm.config';
|
||||
import { getForm } from './IntegrationForm.config';
|
||||
import { prepareForEdit } from './IntegrationForm.helpers';
|
||||
import styles from './IntegrationForm.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface IntegrationFormProps {
|
||||
id: AlertReceiveChannel['id'] | 'new';
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'] | 'new';
|
||||
isTableView?: boolean;
|
||||
onHide: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
navigateToAlertGroupLabels: (id: AlertReceiveChannel['id']) => void;
|
||||
navigateToAlertGroupLabels: (id: ApiSchemas['AlertReceiveChannel']['id']) => void;
|
||||
}
|
||||
|
||||
export const IntegrationForm = observer((props: IntegrationFormProps) => {
|
||||
|
|
@ -61,18 +59,21 @@ export const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
const {
|
||||
alertReceiveChannelStore,
|
||||
userStore: { currentUser: user },
|
||||
grafanaTeamStore,
|
||||
} = store;
|
||||
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [showNewIntegrationForm, setShowNewIntegrationForm] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<AlertReceiveChannelOption>(undefined);
|
||||
const [selectedOption, setSelectedOption] = useState<ApiSchemas['AlertReceiveChannelIntegrationOptions']>(undefined);
|
||||
const [showIntegrarionsListDrawer, setShowIntegrarionsListDrawer] = useState(id === 'new');
|
||||
const [allContactPoints, setAllContactPoints] = useState([]);
|
||||
const [errors, setErrors] = useState<Record<string, any>>();
|
||||
|
||||
const form = useMemo(() => getForm(grafanaTeamStore), [grafanaTeamStore]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setAllContactPoints(await alertReceiveChannelStore.getGrafanaAlertingContactPoints());
|
||||
setAllContactPoints(await AlertReceiveChannelHelper.getGrafanaAlertingContactPoints());
|
||||
})();
|
||||
}, []);
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ export const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
const { alertReceiveChannelOptions } = alertReceiveChannelStore;
|
||||
|
||||
const options = alertReceiveChannelOptions
|
||||
? alertReceiveChannelOptions.filter((option: AlertReceiveChannelOption) => {
|
||||
? alertReceiveChannelOptions.filter((option: ApiSchemas['AlertReceiveChannelIntegrationOptions']) => {
|
||||
if (option.value === 'grafana_alerting' && !window.grafanaBootData.settings.unifiedAlertingEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -219,58 +220,36 @@ export const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
if (isCreate) {
|
||||
await createNewIntegration();
|
||||
} else {
|
||||
await alertReceiveChannelStore.update(id, data, undefined, true);
|
||||
await alertReceiveChannelStore.update({ id, data, skipErrorHandling: true });
|
||||
}
|
||||
} catch (error) {
|
||||
setErrors(error.response.data);
|
||||
|
||||
openErrorNotification(
|
||||
`There was an issue ${isCreate ? 'creating' : 'updating'} the integration. Please try again.`
|
||||
);
|
||||
setErrors(error);
|
||||
return;
|
||||
}
|
||||
|
||||
await onSubmit();
|
||||
onHide();
|
||||
|
||||
function createNewIntegration(): Promise<void | AlertReceiveChannel> {
|
||||
let promise = alertReceiveChannelStore.create<AlertReceiveChannel>(data, true);
|
||||
|
||||
async function createNewIntegration(): Promise<void | ApiSchemas['AlertReceiveChannel']> {
|
||||
const response = await alertReceiveChannelStore.create({ data, skipErrorHandling: true });
|
||||
const pushHistory = (id) => history.push(`${PLUGIN_ROOT}/integrations/${id}`);
|
||||
|
||||
promise
|
||||
.then((response) => {
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IntegrationHelper.isSpecificIntegration(selectedOption.value, 'grafana_alerting')) {
|
||||
return pushHistory(response.id);
|
||||
}
|
||||
|
||||
return (
|
||||
data.is_existing
|
||||
? alertReceiveChannelStore.connectContactPoint(response.id, data.alert_manager, data.contact_point)
|
||||
: alertReceiveChannelStore.createContactPoint(response.id, data.alert_manager, data.contact_point)
|
||||
)
|
||||
.catch(onCatch)
|
||||
.finally(() => pushHistory(response.id));
|
||||
})
|
||||
.catch(onCatch);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function onCatch(err: any) {
|
||||
if (err.response?.data?.length > 0) {
|
||||
openErrorNotification(err.response.data);
|
||||
} else {
|
||||
openErrorNotification('Something went wrong, please try again later.');
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IntegrationHelper.isSpecificIntegration(selectedOption.value, 'grafana_alerting')) {
|
||||
pushHistory(response.id);
|
||||
}
|
||||
|
||||
await (data.is_existing
|
||||
? AlertReceiveChannelHelper.connectContactPoint
|
||||
: AlertReceiveChannelHelper.createContactPoint)(response.id, data.alert_manager, data.contact_point);
|
||||
|
||||
pushHistory(response.id);
|
||||
}
|
||||
}
|
||||
|
||||
function onBlockClick(option: AlertReceiveChannelOption) {
|
||||
function onBlockClick(option: ApiSchemas['AlertReceiveChannelIntegrationOptions']) {
|
||||
setSelectedOption(option);
|
||||
setShowNewIntegrationForm(true);
|
||||
setShowIntegrarionsListDrawer(false);
|
||||
|
|
@ -338,11 +317,9 @@ const CustomFieldSectionRenderer: React.FC<CustomFieldSectionRendererProps> = ({
|
|||
}
|
||||
);
|
||||
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const response = await alertReceiveChannelStore.getGrafanaAlertingContactPoints();
|
||||
const response = await AlertReceiveChannelHelper.getGrafanaAlertingContactPoints();
|
||||
setState({
|
||||
allContactPoints: response,
|
||||
dataSources: response.map((res) => ({ label: res.name, value: res.uid })),
|
||||
|
|
@ -445,7 +422,9 @@ const CustomFieldSectionRenderer: React.FC<CustomFieldSectionRendererProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const HowTheIntegrationWorks: React.FC<{ selectedOption: AlertReceiveChannelOption }> = ({ selectedOption }) => {
|
||||
const HowTheIntegrationWorks: React.FC<{ selectedOption: ApiSchemas['AlertReceiveChannelIntegrationOptions'] }> = ({
|
||||
selectedOption,
|
||||
}) => {
|
||||
if (!selectedOption) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -487,8 +466,8 @@ const HowTheIntegrationWorks: React.FC<{ selectedOption: AlertReceiveChannelOpti
|
|||
};
|
||||
|
||||
const IntegrationBlocks: React.FC<{
|
||||
options: AlertReceiveChannelOption[];
|
||||
onBlockClick: (option: AlertReceiveChannelOption) => void;
|
||||
options: Array<ApiSchemas['AlertReceiveChannelIntegrationOptions']>;
|
||||
onBlockClick: (option: ApiSchemas['AlertReceiveChannelIntegrationOptions']) => void;
|
||||
}> = ({ options, onBlockClick }) => {
|
||||
return (
|
||||
<div className={cx('cards')} data-testid="create-integration-modal">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { getIsTooManyLabelsWarningVisible } from './IntegrationLabelsForm.helpers';
|
||||
|
||||
describe('getIsTooManyLabelsWarningVisible()', () => {
|
||||
const CUSTOM_LABEL = { key: { id: 'c', name: 'c' }, value: { id: 'c', name: 'c' } };
|
||||
const CUSTOM_LABEL = {
|
||||
key: { id: 'c', name: 'c', prescribed: false },
|
||||
value: { id: 'c', name: 'c', prescribed: false },
|
||||
};
|
||||
|
||||
it('should return false if limit is not exceeded', () => {
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
||||
const countNumberOfInheritedAndCustomLabels = (alert_group_labels: AlertReceiveChannel['alert_group_labels']) => {
|
||||
const countNumberOfInheritedAndCustomLabels = (
|
||||
alert_group_labels: ApiSchemas['AlertReceiveChannel']['alert_group_labels']
|
||||
) => {
|
||||
const inheritedCount = alert_group_labels.inheritable
|
||||
? Object.keys(alert_group_labels.inheritable).filter((labelKey) => alert_group_labels.inheritable?.[labelKey])
|
||||
.length
|
||||
|
|
@ -10,11 +12,11 @@ const countNumberOfInheritedAndCustomLabels = (alert_group_labels: AlertReceiveC
|
|||
};
|
||||
|
||||
export const getIsTooManyLabelsWarningVisible = (
|
||||
alert_group_labels: AlertReceiveChannel['alert_group_labels'],
|
||||
alert_group_labels: ApiSchemas['AlertReceiveChannel']['alert_group_labels'],
|
||||
limit = 15
|
||||
) => countNumberOfInheritedAndCustomLabels(alert_group_labels) > limit;
|
||||
|
||||
export const getIsAddBtnDisabled = ({ custom }: AlertReceiveChannel['alert_group_labels']) => {
|
||||
export const getIsAddBtnDisabled = ({ custom }: ApiSchemas['AlertReceiveChannel']['alert_group_labels']) => {
|
||||
const lastItem = custom.at(-1);
|
||||
return lastItem && (lastItem?.key.id === undefined || lastItem?.value.id === undefined);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { PluginLink } from 'components/PluginLink/PluginLink';
|
|||
import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { IntegrationTemplate } from 'containers/IntegrationTemplate/IntegrationTemplate';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { splitToGroups } from 'models/label/label.helpers';
|
||||
import { LabelsErrors } from 'models/label/label.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
|
@ -39,10 +38,10 @@ const cx = cn.bind(styles);
|
|||
const INPUT_WIDTH = 280;
|
||||
|
||||
interface IntegrationLabelsFormProps {
|
||||
id: AlertReceiveChannel['id'];
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
onSubmit: () => void;
|
||||
onHide: () => void;
|
||||
onOpenIntegrationSettings: (id: AlertReceiveChannel['id']) => void;
|
||||
onOpenIntegrationSettings: (id: ApiSchemas['AlertReceiveChannel']['id']) => void;
|
||||
}
|
||||
|
||||
export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
|
||||
|
|
@ -63,7 +62,9 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
|||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await alertReceiveChannelStore.saveAlertReceiveChannel(id, { alert_group_labels: alertGroupLabels });
|
||||
await alertReceiveChannelStore.saveAlertReceiveChannel(id, {
|
||||
alert_group_labels: alertGroupLabels,
|
||||
});
|
||||
onSubmit();
|
||||
onHide();
|
||||
} catch (err) {
|
||||
|
|
@ -246,9 +247,9 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
|||
});
|
||||
|
||||
interface CustomLabelsProps {
|
||||
alertGroupLabels: AlertReceiveChannel['alert_group_labels'];
|
||||
alertGroupLabels: ApiSchemas['AlertReceiveChannel']['alert_group_labels'];
|
||||
customLabelsErrors: LabelsErrors;
|
||||
onChange: (value: AlertReceiveChannel['alert_group_labels']) => void;
|
||||
onChange: (value: ApiSchemas['AlertReceiveChannel']['alert_group_labels']) => void;
|
||||
onShowTemplateEditor: (index: number) => void;
|
||||
}
|
||||
|
||||
|
|
@ -263,8 +264,8 @@ const CustomLabels = (props: CustomLabelsProps) => {
|
|||
custom: [
|
||||
...alertGroupLabels.custom,
|
||||
{
|
||||
key: { id: undefined, name: undefined },
|
||||
value: { id: undefined, name: undefined },
|
||||
key: { id: undefined, name: undefined, prescribed: false },
|
||||
value: { id: undefined, name: undefined, prescribed: false },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -275,8 +276,8 @@ const CustomLabels = (props: CustomLabelsProps) => {
|
|||
custom: [
|
||||
...alertGroupLabels.custom,
|
||||
{
|
||||
key: { id: undefined, name: undefined },
|
||||
value: { id: null, name: undefined }, // id = null means it's a templated value
|
||||
key: { id: undefined, name: undefined, prescribed: false },
|
||||
value: { id: null, name: undefined, prescribed: false }, // id = null means it's a templated value
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import { Text } from 'components/Text/Text';
|
|||
import { TemplateResult } from 'containers/TemplateResult/TemplateResult';
|
||||
import { TemplatesAlertGroupsList, TEMPLATE_PAGE } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { IntegrationTemplateOptions, LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { LocationHelper } from 'utils/LocationHelper';
|
||||
|
|
@ -34,7 +34,7 @@ import styles from './IntegrationTemplate.module.scss';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface IntegrationTemplateProps {
|
||||
id: AlertReceiveChannel['id'];
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
channelFilterId?: ChannelFilter['id'];
|
||||
template: TemplateForEdit;
|
||||
templateBody: string;
|
||||
|
|
@ -49,7 +49,7 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
|
|||
|
||||
const [isCheatSheetVisible, setIsCheatSheetVisible] = useState<boolean>(false);
|
||||
const [chatOpsPermalink, setChatOpsPermalink] = useState(undefined);
|
||||
const [alertGroupPayload, setAlertGroupPayload] = useState<JSON>(undefined);
|
||||
const [alertGroupPayload, setAlertGroupPayload] = useState<{ [key: string]: unknown }>(undefined);
|
||||
const [changedTemplateBody, setChangedTemplateBody] = useState<string>(templateBody);
|
||||
const [resultError, setResultError] = useState<string>(undefined);
|
||||
const [isRecentAlertGroupExisting, setIsRecentAlertGroupExisting] = useState<boolean>(false);
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import React from 'react';
|
|||
import { SelectableValue } from '@grafana/data';
|
||||
import Emoji from 'react-emoji-render';
|
||||
|
||||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
import { FormItemType } from 'components/GForm/GForm.types';
|
||||
import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
|
||||
export const form: { name: string; fields: FormItem[] } = {
|
||||
export const getForm = (alertReceiveChannelStore: AlertReceiveChannelStore) => ({
|
||||
name: 'Maintenance',
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -15,11 +17,15 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
type: FormItemType.GSelect,
|
||||
validation: { required: true },
|
||||
extra: {
|
||||
modelName: 'alertReceiveChannelStore',
|
||||
items: alertReceiveChannelStore.items,
|
||||
fetchItemsFn: alertReceiveChannelStore.fetchItems,
|
||||
fetchItemFn: alertReceiveChannelStore.fetchItemById,
|
||||
getSearchResult: () => AlertReceiveChannelHelper.getSearchResult(alertReceiveChannelStore),
|
||||
displayField: 'verbal_name',
|
||||
valueField: 'id',
|
||||
showSearch: true,
|
||||
getOptionLabel: (item: SelectableValue) => <Emoji text={item?.label || ''} />,
|
||||
disabled: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -77,4 +83,4 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import { observer } from 'mobx-react';
|
|||
|
||||
import { GForm } from 'components/GForm/GForm';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { openNotification, showApiError } from 'utils/utils';
|
||||
|
||||
import { form } from './MaintenanceForm.config';
|
||||
import { getForm } from './MaintenanceForm.config';
|
||||
|
||||
import styles from './MaintenanceForm.module.css';
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface MaintenanceFormProps {
|
||||
initialData: {
|
||||
alert_receive_channel_id?: AlertReceiveChannel['id'];
|
||||
alert_receive_channel_id?: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
disabled?: boolean;
|
||||
};
|
||||
onHide: () => void;
|
||||
|
|
@ -29,20 +30,17 @@ interface MaintenanceFormProps {
|
|||
|
||||
export const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
||||
const { onUpdate, onHide, initialData = {} } = props;
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const form = useMemo(() => getForm(alertReceiveChannelStore), [alertReceiveChannelStore]);
|
||||
const maintenanceForm = useMemo(() => (initialData.disabled ? cloneDeep(form) : form), [initialData]);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
const handleSubmit = useCallback(async (data) => {
|
||||
try {
|
||||
await alertReceiveChannelStore.startMaintenanceMode(
|
||||
await AlertReceiveChannelHelper.startMaintenanceMode(
|
||||
initialData.alert_receive_channel_id,
|
||||
data.mode,
|
||||
data.duration
|
||||
);
|
||||
|
||||
onHide();
|
||||
onUpdate();
|
||||
openNotification('Maintenance has been started');
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { SelectableValue } from '@grafana/data';
|
|||
import Emoji from 'react-emoji-render';
|
||||
|
||||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
|
||||
import { OutgoingWebhookPreset } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { generateAssignToTeamInputDescription } from 'utils/consts';
|
||||
import { KeyValuePair } from 'utils/utils';
|
||||
|
|
@ -21,10 +24,17 @@ export const WebhookTriggerType = {
|
|||
Unacknowledged: new KeyValuePair('7', 'Unacknowledged'),
|
||||
};
|
||||
|
||||
export function createForm(
|
||||
presets: OutgoingWebhookPreset[] = [],
|
||||
hasLabelsFeature?: boolean
|
||||
): {
|
||||
export function createForm({
|
||||
presets = [],
|
||||
grafanaTeamStore,
|
||||
alertReceiveChannelStore,
|
||||
hasLabelsFeature,
|
||||
}: {
|
||||
presets: OutgoingWebhookPreset[];
|
||||
grafanaTeamStore: GrafanaTeamStore;
|
||||
alertReceiveChannelStore: AlertReceiveChannelStore;
|
||||
hasLabelsFeature?: boolean;
|
||||
}): {
|
||||
name: string;
|
||||
fields: FormItem[];
|
||||
} {
|
||||
|
|
@ -50,7 +60,9 @@ export function createForm(
|
|||
)} This setting does not effect execution of the webhook.`,
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'grafanaTeamStore',
|
||||
items: grafanaTeamStore.items,
|
||||
fetchItemsFn: grafanaTeamStore.updateItems,
|
||||
getSearchResult: grafanaTeamStore.getSearchResult,
|
||||
displayField: 'name',
|
||||
valueField: 'id',
|
||||
showSearch: true,
|
||||
|
|
@ -148,7 +160,10 @@ export function createForm(
|
|||
data.trigger_type === WebhookTriggerType.EscalationStep.key,
|
||||
extra: {
|
||||
placeholder: 'Choose (Optional)',
|
||||
modelName: 'alertReceiveChannelStore',
|
||||
items: alertReceiveChannelStore.items,
|
||||
fetchItemsFn: alertReceiveChannelStore.fetchItems,
|
||||
fetchItemFn: alertReceiveChannelStore.fetchItemById,
|
||||
getSearchResult: () => AlertReceiveChannelHelper.getSearchResult(alertReceiveChannelStore),
|
||||
displayField: 'verbal_name',
|
||||
valueField: 'id',
|
||||
showSearch: true,
|
||||
|
|
|
|||
|
|
@ -93,10 +93,15 @@ export const OutgoingWebhookForm = observer((props: OutgoingWebhookFormProps) =>
|
|||
const [selectedPreset, setSelectedPreset] = useState<OutgoingWebhookPreset>(undefined);
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
|
||||
const { outgoingWebhookStore, hasFeature } = useStore();
|
||||
const { outgoingWebhookStore, hasFeature, grafanaTeamStore, alertReceiveChannelStore } = useStore();
|
||||
const isNew = action === WebhookFormActionType.NEW;
|
||||
const isNewOrCopy = isNew || action === WebhookFormActionType.COPY;
|
||||
const form = createForm(outgoingWebhookStore.outgoingWebhookPresets, hasFeature(AppFeature.Labels));
|
||||
const form = createForm({
|
||||
presets: outgoingWebhookStore.outgoingWebhookPresets,
|
||||
grafanaTeamStore,
|
||||
alertReceiveChannelStore,
|
||||
hasLabelsFeature: hasFeature(AppFeature.Labels),
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (data: Partial<OutgoingWebhook>) => {
|
||||
|
|
@ -386,75 +391,76 @@ interface WebhookTabsProps {
|
|||
formElement: React.ReactElement;
|
||||
}
|
||||
|
||||
const WebhookTabsContent: React.FC<WebhookTabsProps> = ({
|
||||
id,
|
||||
action,
|
||||
activeTab,
|
||||
data,
|
||||
onHide,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
formElement,
|
||||
}) => {
|
||||
const [confirmationModal, setConfirmationModal] = useState<ConfirmModalProps>(undefined);
|
||||
const { outgoingWebhookStore, hasFeature } = useStore();
|
||||
const form = createForm(outgoingWebhookStore.outgoingWebhookPresets, hasFeature(AppFeature.Labels));
|
||||
return (
|
||||
<div className={cx('tabs__content')}>
|
||||
{confirmationModal && (
|
||||
<ConfirmModal {...(confirmationModal as ConfirmModalProps)} onDismiss={() => setConfirmationModal(undefined)} />
|
||||
)}
|
||||
const WebhookTabsContent: React.FC<WebhookTabsProps> = observer(
|
||||
({ id, action, activeTab, data, onHide, onUpdate, onDelete, formElement }) => {
|
||||
const [confirmationModal, setConfirmationModal] = useState<ConfirmModalProps>(undefined);
|
||||
const { outgoingWebhookStore, hasFeature, grafanaTeamStore, alertReceiveChannelStore } = useStore();
|
||||
const form = createForm({
|
||||
presets: outgoingWebhookStore.outgoingWebhookPresets,
|
||||
grafanaTeamStore,
|
||||
alertReceiveChannelStore,
|
||||
hasLabelsFeature: hasFeature(AppFeature.Labels),
|
||||
});
|
||||
return (
|
||||
<div className={cx('tabs__content')}>
|
||||
{confirmationModal && (
|
||||
<ConfirmModal
|
||||
{...(confirmationModal as ConfirmModalProps)}
|
||||
onDismiss={() => setConfirmationModal(undefined)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === WebhookTabs.Settings.key && (
|
||||
<>
|
||||
<div className={cx('content')}>
|
||||
{formElement}
|
||||
<div className={cx('buttons')}>
|
||||
<HorizontalGroup justify={'flex-end'}>
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
|
||||
<Button
|
||||
form={form.name}
|
||||
variant="destructive"
|
||||
type="button"
|
||||
disabled={data.is_legacy}
|
||||
onClick={() => {
|
||||
setConfirmationModal({
|
||||
isOpen: true,
|
||||
body: 'The action cannot be undone.',
|
||||
confirmText: 'Delete',
|
||||
dismissText: 'Cancel',
|
||||
onConfirm: onDelete,
|
||||
title: `Are you sure you want to delete webhook?`,
|
||||
} as ConfirmModalProps);
|
||||
}}
|
||||
>
|
||||
Delete Webhook
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
|
||||
<Button form={form.name} type="submit" disabled={data.is_legacy}>
|
||||
{action === WebhookFormActionType.NEW ? 'Create' : 'Update'} Webhook
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
{data.is_legacy ? (
|
||||
{activeTab === WebhookTabs.Settings.key && (
|
||||
<>
|
||||
<div className={cx('content')}>
|
||||
<Text type="secondary">Legacy migrated webhooks are not editable. Make a copy to make changes.</Text>
|
||||
{formElement}
|
||||
<div className={cx('buttons')}>
|
||||
<HorizontalGroup justify={'flex-end'}>
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
|
||||
<Button
|
||||
form={form.name}
|
||||
variant="destructive"
|
||||
type="button"
|
||||
disabled={data.is_legacy}
|
||||
onClick={() => {
|
||||
setConfirmationModal({
|
||||
isOpen: true,
|
||||
body: 'The action cannot be undone.',
|
||||
confirmText: 'Delete',
|
||||
dismissText: 'Cancel',
|
||||
onConfirm: onDelete,
|
||||
title: `Are you sure you want to delete webhook?`,
|
||||
} as ConfirmModalProps);
|
||||
}}
|
||||
>
|
||||
Delete Webhook
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
|
||||
<Button form={form.name} type="submit" disabled={data.is_legacy}>
|
||||
{action === WebhookFormActionType.NEW ? 'Create' : 'Update'} Webhook
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeTab === WebhookTabs.LastRun.key && <OutgoingWebhookStatus id={id} onUpdate={onUpdate} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
{data.is_legacy ? (
|
||||
<div className={cx('content')}>
|
||||
<Text type="secondary">Legacy migrated webhooks are not editable. Make a copy to make changes.</Text>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeTab === WebhookTabs.LastRun.key && <OutgoingWebhookStatus id={id} onUpdate={onUpdate} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const WebhookPresetBlocks: React.FC<{
|
||||
presets: OutgoingWebhookPreset[];
|
||||
|
|
|
|||
|
|
@ -1,88 +1,95 @@
|
|||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config';
|
||||
import { RootStore } from 'state/rootStore';
|
||||
import { generateAssignToTeamInputDescription } from 'utils/consts';
|
||||
|
||||
const assignToTeamDescription = generateAssignToTeamInputDescription('Schedules');
|
||||
|
||||
const commonFields: FormItem[] = [
|
||||
{
|
||||
name: 'slack_channel_id',
|
||||
label: 'Slack channel',
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'slackChannelStore',
|
||||
displayField: 'display_name',
|
||||
showSearch: true,
|
||||
allowClear: true,
|
||||
nullItemName: PRIVATE_CHANNEL_NAME,
|
||||
const getCommonFields = ({ slackChannelStore, userGroupStore }: RootStore): FormItem[] =>
|
||||
[
|
||||
{
|
||||
name: 'slack_channel_id',
|
||||
label: 'Slack channel',
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
items: slackChannelStore.items,
|
||||
fetchItemsFn: slackChannelStore.updateItems,
|
||||
fetchItemFn: slackChannelStore.updateItem,
|
||||
getSearchResult: slackChannelStore.getSearchResult,
|
||||
displayField: 'display_name',
|
||||
showSearch: true,
|
||||
allowClear: true,
|
||||
nullItemName: PRIVATE_CHANNEL_NAME,
|
||||
},
|
||||
description:
|
||||
'Calendar parsing errors and notifications about the new on-call shift will be published in this channel.',
|
||||
},
|
||||
description:
|
||||
'Calendar parsing errors and notifications about the new on-call shift will be published in this channel.',
|
||||
},
|
||||
{
|
||||
name: 'user_group',
|
||||
label: 'Slack user group',
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'userGroupStore',
|
||||
displayField: 'handle',
|
||||
showSearch: true,
|
||||
allowClear: true,
|
||||
{
|
||||
name: 'user_group',
|
||||
label: 'Slack user group',
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
items: userGroupStore.items,
|
||||
fetchItemsFn: userGroupStore.updateItems,
|
||||
getSearchResult: userGroupStore.getSearchResult,
|
||||
displayField: 'handle',
|
||||
showSearch: true,
|
||||
allowClear: true,
|
||||
},
|
||||
description:
|
||||
'Group members will be automatically updated with current on-call. In case you want to ping on-call with @group_name.',
|
||||
},
|
||||
description:
|
||||
'Group members will be automatically updated with current on-call. In case you want to ping on-call with @group_name.',
|
||||
},
|
||||
{
|
||||
name: 'notify_oncall_shift_freq',
|
||||
label: 'Notification frequency',
|
||||
type: FormItemType.RemoteSelect,
|
||||
normalize: (value) => value,
|
||||
extra: {
|
||||
href: '/schedules/notify_oncall_shift_freq_options/',
|
||||
displayField: 'display_name',
|
||||
openMenuOnFocus: false,
|
||||
{
|
||||
name: 'notify_oncall_shift_freq',
|
||||
label: 'Notification frequency',
|
||||
type: FormItemType.RemoteSelect,
|
||||
normalize: (value) => value,
|
||||
extra: {
|
||||
href: '/schedules/notify_oncall_shift_freq_options/',
|
||||
displayField: 'display_name',
|
||||
openMenuOnFocus: false,
|
||||
},
|
||||
description: 'Specify the frequency that shift notifications are sent to scheduled team members.',
|
||||
},
|
||||
description: 'Specify the frequency that shift notifications are sent to scheduled team members.',
|
||||
},
|
||||
{
|
||||
name: 'notify_empty_oncall',
|
||||
label: 'Action for slot when no one is on-call',
|
||||
type: FormItemType.RemoteSelect,
|
||||
normalize: (value) => value,
|
||||
extra: {
|
||||
href: '/schedules/notify_empty_oncall_options/',
|
||||
displayField: 'display_name',
|
||||
openMenuOnFocus: false,
|
||||
{
|
||||
name: 'notify_empty_oncall',
|
||||
label: 'Action for slot when no one is on-call',
|
||||
type: FormItemType.RemoteSelect,
|
||||
normalize: (value) => value,
|
||||
extra: {
|
||||
href: '/schedules/notify_empty_oncall_options/',
|
||||
displayField: 'display_name',
|
||||
openMenuOnFocus: false,
|
||||
},
|
||||
description: 'Specify how to notify team members when there is no one scheduled for an on-call shift.',
|
||||
},
|
||||
description: 'Specify how to notify team members when there is no one scheduled for an on-call shift.',
|
||||
},
|
||||
{
|
||||
name: 'mention_oncall_start',
|
||||
label: 'Current shift notification settings',
|
||||
type: FormItemType.RemoteSelect,
|
||||
normalize: (value) => value,
|
||||
extra: {
|
||||
href: '/schedules/mention_options/',
|
||||
displayField: 'display_name',
|
||||
openMenuOnFocus: false,
|
||||
{
|
||||
name: 'mention_oncall_start',
|
||||
label: 'Current shift notification settings',
|
||||
type: FormItemType.RemoteSelect,
|
||||
normalize: (value) => value,
|
||||
extra: {
|
||||
href: '/schedules/mention_options/',
|
||||
displayField: 'display_name',
|
||||
openMenuOnFocus: false,
|
||||
},
|
||||
description: 'Specify how to notify a team member when their on-call shift begins ',
|
||||
},
|
||||
description: 'Specify how to notify a team member when their on-call shift begins ',
|
||||
},
|
||||
{
|
||||
name: 'mention_oncall_next',
|
||||
label: 'Next shift notification settings',
|
||||
type: FormItemType.RemoteSelect,
|
||||
normalize: (value) => value,
|
||||
extra: {
|
||||
href: '/schedules/mention_options/',
|
||||
displayField: 'display_name',
|
||||
openMenuOnFocus: false,
|
||||
{
|
||||
name: 'mention_oncall_next',
|
||||
label: 'Next shift notification settings',
|
||||
type: FormItemType.RemoteSelect,
|
||||
normalize: (value) => value,
|
||||
extra: {
|
||||
href: '/schedules/mention_options/',
|
||||
displayField: 'display_name',
|
||||
openMenuOnFocus: false,
|
||||
},
|
||||
description: 'Specify how to notify a team member when their shift is the next one scheduled',
|
||||
},
|
||||
description: 'Specify how to notify a team member when their shift is the next one scheduled',
|
||||
},
|
||||
].map((field) => ({ ...field, collapsed: true }));
|
||||
].map((field) => ({ ...field, collapsed: true }));
|
||||
|
||||
export const iCalForm: { name: string; fields: FormItem[] } = {
|
||||
export const getICalForm = (rootStore: RootStore) => ({
|
||||
name: 'Schedule',
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -96,7 +103,9 @@ export const iCalForm: { name: string; fields: FormItem[] } = {
|
|||
description: assignToTeamDescription,
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'grafanaTeamStore',
|
||||
items: rootStore.grafanaTeamStore.items,
|
||||
fetchItemsFn: rootStore.grafanaTeamStore.updateItems,
|
||||
getSearchResult: rootStore.grafanaTeamStore.getSearchResult,
|
||||
displayField: 'name',
|
||||
valueField: 'id',
|
||||
showSearch: true,
|
||||
|
|
@ -117,11 +126,11 @@ export const iCalForm: { name: string; fields: FormItem[] } = {
|
|||
extra: { rows: 2 },
|
||||
},
|
||||
|
||||
...commonFields,
|
||||
...getCommonFields(rootStore),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
export const calendarForm: { name: string; fields: FormItem[] } = {
|
||||
export const getCalendarForm = (rootStore: RootStore) => ({
|
||||
name: 'Schedule',
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -135,7 +144,9 @@ export const calendarForm: { name: string; fields: FormItem[] } = {
|
|||
description: assignToTeamDescription,
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'grafanaTeamStore',
|
||||
items: rootStore.grafanaTeamStore.items,
|
||||
fetchItemsFn: rootStore.grafanaTeamStore.updateItems,
|
||||
getSearchResult: rootStore.grafanaTeamStore.getSearchResult,
|
||||
displayField: 'name',
|
||||
valueField: 'id',
|
||||
showSearch: true,
|
||||
|
|
@ -157,11 +168,11 @@ export const calendarForm: { name: string; fields: FormItem[] } = {
|
|||
extra: { rows: 2 },
|
||||
},
|
||||
|
||||
...commonFields,
|
||||
...getCommonFields(rootStore),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
export const apiForm: { name: string; fields: FormItem[] } = {
|
||||
export const getApiForm = (rootStore: RootStore) => ({
|
||||
name: 'Schedule',
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -175,13 +186,15 @@ export const apiForm: { name: string; fields: FormItem[] } = {
|
|||
description: assignToTeamDescription,
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'grafanaTeamStore',
|
||||
items: rootStore.grafanaTeamStore.items,
|
||||
fetchItemsFn: rootStore.grafanaTeamStore.updateItems,
|
||||
getSearchResult: rootStore.grafanaTeamStore.getSearchResult,
|
||||
displayField: 'name',
|
||||
valueField: 'id',
|
||||
showSearch: true,
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
...commonFields,
|
||||
...getCommonFields(rootStore),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { useStore } from 'state/useStore';
|
|||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { openWarningNotification } from 'utils/utils';
|
||||
|
||||
import { apiForm, calendarForm, iCalForm } from './ScheduleForm.config';
|
||||
import { getApiForm, getCalendarForm, getICalForm } from './ScheduleForm.config';
|
||||
import { prepareForEdit } from './ScheduleForm.helpers';
|
||||
|
||||
import styles from './ScheduleForm.module.css';
|
||||
|
|
@ -25,12 +25,6 @@ interface ScheduleFormProps {
|
|||
type?: ScheduleType;
|
||||
}
|
||||
|
||||
const scheduleTypeToForm = {
|
||||
[ScheduleType.Calendar]: calendarForm,
|
||||
[ScheduleType.Ical]: iCalForm,
|
||||
[ScheduleType.API]: apiForm,
|
||||
};
|
||||
|
||||
export const ScheduleForm = observer((props: ScheduleFormProps) => {
|
||||
const { id, type, onSubmit, onHide } = props;
|
||||
const isNew = id === 'new';
|
||||
|
|
@ -39,6 +33,15 @@ export const ScheduleForm = observer((props: ScheduleFormProps) => {
|
|||
|
||||
const { scheduleStore, userStore } = store;
|
||||
|
||||
const scheduleTypeToForm = useMemo(
|
||||
() => ({
|
||||
[ScheduleType.Calendar]: getCalendarForm(store),
|
||||
[ScheduleType.Ical]: getICalForm(store),
|
||||
[ScheduleType.API]: getApiForm(store),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const data = useMemo(() => {
|
||||
return isNew ? { team: userStore.currentUser?.current_team, type } : prepareForEdit(scheduleStore.items[id]);
|
||||
}, [id]);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import cn from 'classnames/bind';
|
|||
import { observer } from 'mobx-react';
|
||||
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { useDebouncedCallback } from 'utils/hooks';
|
||||
|
|
@ -23,8 +24,8 @@ interface TemplatePreviewProps {
|
|||
templateBody: string | null;
|
||||
templateType?: 'plain' | 'html' | 'image' | 'boolean';
|
||||
templateIsRoute?: boolean;
|
||||
payload?: JSON;
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
payload?: { [key: string]: unknown };
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
alertGroupId?: Alert['pk'];
|
||||
outgoingWebhookId?: OutgoingWebhook['id'];
|
||||
templatePage: TEMPLATE_PAGE;
|
||||
|
|
@ -58,14 +59,14 @@ export const TemplatePreview = observer((props: TemplatePreviewProps) => {
|
|||
const [conditionalResult, setConditionalResult] = useState<ConditionalResult>({});
|
||||
|
||||
const store = useStore();
|
||||
const { alertReceiveChannelStore, alertGroupStore, outgoingWebhookStore } = store;
|
||||
const { alertGroupStore, outgoingWebhookStore } = store;
|
||||
|
||||
const handleTemplateBodyChange = useDebouncedCallback(() => {
|
||||
(templatePage === TEMPLATE_PAGE.Webhooks
|
||||
? outgoingWebhookStore.renderPreview(outgoingWebhookId, templateName, templateBody, payload)
|
||||
: alertGroupId
|
||||
? alertGroupStore.renderPreview(alertGroupId, templateName, templateBody)
|
||||
: alertReceiveChannelStore.renderPreview(alertReceiveChannelId, templateName, templateBody, payload)
|
||||
: AlertReceiveChannelHelper.renderPreview(alertReceiveChannelId, templateName, templateBody, payload)
|
||||
)
|
||||
.then((data) => {
|
||||
setResult(data);
|
||||
|
|
|
|||
|
|
@ -8,19 +8,19 @@ import { Block } from 'components/GBlock/Block';
|
|||
import { Text } from 'components/Text/Text';
|
||||
import styles from 'containers/IntegrationTemplate/IntegrationTemplate.module.scss';
|
||||
import { TemplatePreview, TEMPLATE_PAGE } from 'containers/TemplatePreview/TemplatePreview';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface ResultProps {
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'];
|
||||
alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
outgoingWebhookId?: OutgoingWebhook['id'];
|
||||
templateBody: string;
|
||||
template: TemplateForEdit;
|
||||
isAlertGroupExisting?: boolean;
|
||||
chatOpsPermalink?: string;
|
||||
payload?: JSON;
|
||||
payload?: { [key: string]: unknown };
|
||||
error?: string;
|
||||
onSaveAndFollowLink?: (link: string) => void;
|
||||
templateIsRoute?: boolean;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import { MonacoEditor, MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEdi
|
|||
import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { OutgoingWebhook, OutgoingWebhookResponse } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './TemplatesAlertGroupsList.module.css';
|
||||
|
|
@ -26,7 +26,7 @@ export enum TEMPLATE_PAGE {
|
|||
interface TemplatesAlertGroupsListProps {
|
||||
templatePage: TEMPLATE_PAGE;
|
||||
templates: AlertTemplatesDTO[];
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'];
|
||||
alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
outgoingwebhookId?: OutgoingWebhook['id'];
|
||||
heading?: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,186 @@
|
|||
import { AlertReceiveChannel } from './alert_receive_channel.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
|
||||
import { makeRequest } from 'network/network';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { onCallApi } from 'network/oncall-api/http-client';
|
||||
import { SelectOption } from 'state/types';
|
||||
import { showApiError } from 'utils/utils';
|
||||
|
||||
export function getAlertReceiveChannelDisplayName(alertReceiveChannel?: AlertReceiveChannel, withDescription = true) {
|
||||
if (!alertReceiveChannel) {
|
||||
return '';
|
||||
import { AlertReceiveChannelStore } from './alert_receive_channel';
|
||||
import { MaintenanceMode } from './alert_receive_channel.types';
|
||||
|
||||
export class AlertReceiveChannelHelper {
|
||||
static getAlertReceiveChannelDisplayName(
|
||||
alertReceiveChannel?: ApiSchemas['AlertReceiveChannel'],
|
||||
withDescription = true
|
||||
) {
|
||||
if (!alertReceiveChannel) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return withDescription && alertReceiveChannel.description
|
||||
? `${alertReceiveChannel.verbal_name} (${alertReceiveChannel.description})`
|
||||
: alertReceiveChannel.verbal_name;
|
||||
}
|
||||
|
||||
return withDescription && alertReceiveChannel.description
|
||||
? `${alertReceiveChannel.verbal_name} (${alertReceiveChannel.description})`
|
||||
: alertReceiveChannel.verbal_name;
|
||||
static getSearchResult(store: AlertReceiveChannelStore) {
|
||||
return store.searchResult
|
||||
? store.searchResult.map(
|
||||
(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']) => store.items?.[alertReceiveChannelId]
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
static getPaginatedSearchResult(store: AlertReceiveChannelStore) {
|
||||
return store.paginatedSearchResult
|
||||
? {
|
||||
page_size: store.paginatedSearchResult.page_size,
|
||||
count: store.paginatedSearchResult.count,
|
||||
results: store.paginatedSearchResult.results?.map(
|
||||
(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']) => store.items?.[alertReceiveChannelId]
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
static getIntegration(
|
||||
store: AlertReceiveChannelStore,
|
||||
alertReceiveChannel: Partial<ApiSchemas['AlertReceiveChannel'] | ApiSchemas['FastAlertReceiveChannel']>
|
||||
): SelectOption {
|
||||
return (
|
||||
store.alertReceiveChannelOptions &&
|
||||
alertReceiveChannel &&
|
||||
store.alertReceiveChannelOptions.find(
|
||||
(alertReceiveChannelOption: SelectOption) => alertReceiveChannelOption.value === alertReceiveChannel.integration
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
static async deleteAlertReceiveChannel(id: ApiSchemas['AlertReceiveChannel']['id']) {
|
||||
return (await onCallApi().DELETE('/alert_receive_channels/{id}/', { params: { path: { id } } })).data;
|
||||
}
|
||||
|
||||
static async getGrafanaAlertingContactPoints() {
|
||||
return (await onCallApi().GET('/alert_receive_channels/contact_points/', undefined)).data;
|
||||
}
|
||||
|
||||
static async connectContactPoint(
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'],
|
||||
datasource_uid: string,
|
||||
contact_point_name: string
|
||||
) {
|
||||
return (
|
||||
await onCallApi().POST('/alert_receive_channels/{id}/connect_contact_point/', {
|
||||
params: { path: { id: alertReceiveChannelId } },
|
||||
body: {
|
||||
datasource_uid,
|
||||
contact_point_name,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
static async disconnectContactPoint(
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'],
|
||||
datasource_uid: string,
|
||||
contact_point_name: string
|
||||
) {
|
||||
return (
|
||||
await onCallApi().POST('/alert_receive_channels/{id}/disconnect_contact_point/', {
|
||||
params: { path: { id: alertReceiveChannelId } },
|
||||
body: {
|
||||
datasource_uid,
|
||||
contact_point_name,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
static async createContactPoint(
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'],
|
||||
datasource_uid: string,
|
||||
contact_point_name: string
|
||||
) {
|
||||
return (
|
||||
await onCallApi().POST('/alert_receive_channels/{id}/create_contact_point/', {
|
||||
params: { path: { id: alertReceiveChannelId } },
|
||||
body: {
|
||||
datasource_uid,
|
||||
contact_point_name,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
static async sendDemoAlert(id: ApiSchemas['AlertReceiveChannel']['id'], payload?: { [key: string]: unknown }) {
|
||||
await onCallApi().POST('/alert_receive_channels/{id}/send_demo_alert/', {
|
||||
params: { path: { id } },
|
||||
body: { demo_alert_payload: payload },
|
||||
});
|
||||
}
|
||||
|
||||
static async renderPreview(
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'],
|
||||
template_name: string,
|
||||
template_body: string,
|
||||
payload: { [key: string]: unknown }
|
||||
) {
|
||||
return (
|
||||
await onCallApi().POST('/alertgroups/{id}/preview_template/', {
|
||||
params: { path: { id } },
|
||||
body: { template_name, template_body, payload },
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
static async changeTeam(id: ApiSchemas['AlertReceiveChannel']['id'], teamId: GrafanaTeam['id']) {
|
||||
return (
|
||||
await onCallApi().PUT('/alert_receive_channels/{id}/change_team/', {
|
||||
params: { path: { id }, query: { team_id: String(teamId) } },
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
static async migrateChannel(id: ApiSchemas['AlertReceiveChannel']['id']) {
|
||||
return (await onCallApi().POST('/alert_receive_channels/{id}/migrate/', { params: { path: { id } } })).data;
|
||||
}
|
||||
|
||||
static async startMaintenanceMode(
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'],
|
||||
mode: MaintenanceMode,
|
||||
duration: ApiSchemas['DurationEnum']
|
||||
) {
|
||||
return (
|
||||
await onCallApi().POST('/alert_receive_channels/{id}/start_maintenance/', {
|
||||
params: { path: { id } },
|
||||
body: {
|
||||
mode,
|
||||
duration,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
static async stopMaintenanceMode(id: ApiSchemas['AlertReceiveChannel']['id']) {
|
||||
return (await onCallApi().POST('/alert_receive_channels/{id}/stop_maintenance/', { params: { path: { id } } }))
|
||||
.data;
|
||||
}
|
||||
|
||||
static async sendDemoAlertToParticularRoute(id: ChannelFilter['id']) {
|
||||
await makeRequest(`/channel_filters/${id}/send_demo_alert/`, { method: 'POST' }).catch(showApiError);
|
||||
}
|
||||
|
||||
static async convertRegexpTemplateToJinja2Template(id: ChannelFilter['id']) {
|
||||
const result = await makeRequest(`/channel_filters/${id}/convert_from_regex_to_jinja2/`, { method: 'POST' }).catch(
|
||||
showApiError
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async createChannelFilter(data: Partial<ChannelFilter>) {
|
||||
return await makeRequest('/channel_filters/', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,125 +1,110 @@
|
|||
import { omit } from 'lodash-es';
|
||||
import { action, observable, makeObservable, runInAction } from 'mobx';
|
||||
import { runInAction, makeAutoObservable } from 'mobx';
|
||||
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { BaseStore } from 'models/base_store';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
|
||||
import { Heartbeat } from 'models/heartbeat/heartbeat.types';
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { makeRequest } from 'network/network';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { operations } from 'network/oncall-api/autogenerated-api.types';
|
||||
import { onCallApi } from 'network/oncall-api/http-client';
|
||||
import { move } from 'state/helpers';
|
||||
import { RootStore } from 'state/rootStore';
|
||||
import { SelectOption } from 'state/types';
|
||||
import { RootBaseStore } from 'state/rootBaseStore/RootBaseStore';
|
||||
import { WithGlobalNotification } from 'utils/decorators';
|
||||
import { showApiError } from 'utils/utils';
|
||||
import { OmitReadonlyMembers } from 'utils/types';
|
||||
|
||||
import {
|
||||
AlertReceiveChannel,
|
||||
AlertReceiveChannelOption,
|
||||
AlertReceiveChannelCounters,
|
||||
ContactPoint,
|
||||
MaintenanceMode,
|
||||
SupportedIntegrationFilters,
|
||||
} from './alert_receive_channel.types';
|
||||
import { AlertReceiveChannelCounters, ContactPoint } from './alert_receive_channel.types';
|
||||
|
||||
export class AlertReceiveChannelStore extends BaseStore {
|
||||
@observable.shallow
|
||||
searchResult: Array<AlertReceiveChannel['id']>;
|
||||
|
||||
@observable.shallow
|
||||
paginatedSearchResult: { count?: number; results?: Array<AlertReceiveChannel['id']>; page_size?: number } = {};
|
||||
|
||||
@observable.shallow
|
||||
items: { [id: string]: AlertReceiveChannel } = {};
|
||||
|
||||
@observable.shallow
|
||||
export class AlertReceiveChannelStore {
|
||||
path = '/alert_receive_channels/';
|
||||
rootStore: RootBaseStore;
|
||||
searchResult: Array<ApiSchemas['AlertReceiveChannel']['id']>;
|
||||
paginatedSearchResult: {
|
||||
count?: number;
|
||||
results?: Array<ApiSchemas['AlertReceiveChannel']['id']>;
|
||||
page_size?: number;
|
||||
} = {};
|
||||
items: {
|
||||
[id: string]: ApiSchemas['AlertReceiveChannel'];
|
||||
} = {};
|
||||
counters: { [id: string]: AlertReceiveChannelCounters } = {};
|
||||
|
||||
@observable
|
||||
channelFilterIds: { [id: string]: Array<ChannelFilter['id']> } = {};
|
||||
|
||||
@observable.shallow
|
||||
channelFilters: { [id: string]: ChannelFilter } = {};
|
||||
|
||||
@observable
|
||||
alertReceiveChannelToHeartbeat: {
|
||||
[id: string]: Heartbeat['id'];
|
||||
} = {};
|
||||
|
||||
@observable.shallow
|
||||
actions: { [id: string]: OutgoingWebhook[] } = {};
|
||||
|
||||
@observable.shallow
|
||||
alertReceiveChannelOptions: AlertReceiveChannelOption[] = [];
|
||||
|
||||
@observable.shallow
|
||||
alertReceiveChannelOptions: Array<ApiSchemas['AlertReceiveChannelIntegrationOptions']> = [];
|
||||
templates: { [id: string]: AlertTemplatesDTO[] } = {};
|
||||
|
||||
@observable
|
||||
connectedContactPoints: { [id: string]: ContactPoint[] } = {};
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore);
|
||||
|
||||
makeObservable(this);
|
||||
|
||||
this.path = '/alert_receive_channels/';
|
||||
constructor(rootStore: RootBaseStore) {
|
||||
makeAutoObservable(this, undefined, { autoBind: true });
|
||||
this.rootStore = rootStore;
|
||||
}
|
||||
|
||||
getSearchResult(_query = '') {
|
||||
if (!this.searchResult) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.searchResult.map(
|
||||
(alertReceiveChannelId: AlertReceiveChannel['id']) => this.items?.[alertReceiveChannelId]
|
||||
);
|
||||
@WithGlobalNotification({ failure: 'There was an issue creating Integration. Please try again.' })
|
||||
async create({ data, skipErrorHandling }: { data: ApiSchemas['AlertReceiveChannel']; skipErrorHandling?: boolean }) {
|
||||
const result = await onCallApi({ skipErrorHandling }).POST('/alert_receive_channels/', {
|
||||
params: {},
|
||||
body: data,
|
||||
});
|
||||
await this.rootStore.organizationStore.loadCurrentOrganization();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
getPaginatedSearchResult(_query = '') {
|
||||
if (!this.paginatedSearchResult) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
page_size: this.paginatedSearchResult.page_size,
|
||||
count: this.paginatedSearchResult.count,
|
||||
results: this.paginatedSearchResult.results?.map(
|
||||
(alertReceiveChannelId: AlertReceiveChannel['id']) => this.items?.[alertReceiveChannelId]
|
||||
),
|
||||
};
|
||||
@WithGlobalNotification({ failure: 'There was an issue updating Integration. Please try again.' })
|
||||
async update({
|
||||
id,
|
||||
data,
|
||||
skipErrorHandling,
|
||||
}: {
|
||||
id: ApiSchemas['AlertReceiveChannelUpdate']['id'];
|
||||
data: ApiSchemas['AlertReceiveChannelUpdate'];
|
||||
skipErrorHandling?: boolean;
|
||||
}) {
|
||||
const result = await onCallApi({ skipErrorHandling }).PUT('/alert_receive_channels/{id}/', {
|
||||
params: { path: { id } },
|
||||
body: data,
|
||||
});
|
||||
await this.rootStore.organizationStore.loadCurrentOrganization();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
@action
|
||||
async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling = false): Promise<AlertReceiveChannel> {
|
||||
const alertReceiveChannel = await this.getById(id, skipErrorHandling);
|
||||
async fetchItemById(
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'],
|
||||
skipErrorHandling = false
|
||||
): Promise<ApiSchemas['AlertReceiveChannel']> {
|
||||
const alertReceiveChannel = await onCallApi({ skipErrorHandling }).GET('/alert_receive_channels/{id}/', {
|
||||
params: { path: { id } },
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
// @ts-ignore
|
||||
this.items = {
|
||||
...this.items,
|
||||
[id]: omit(alertReceiveChannel, 'heartbeat'),
|
||||
[id]: { ...alertReceiveChannel.data, heartbeat: alertReceiveChannel.data.heartbeat || null },
|
||||
};
|
||||
});
|
||||
|
||||
this.populateHearbeats([alertReceiveChannel]);
|
||||
this.populateHearbeats([alertReceiveChannel.data]);
|
||||
|
||||
return alertReceiveChannel;
|
||||
return alertReceiveChannel.data;
|
||||
}
|
||||
|
||||
@action
|
||||
async updateItems(query: any = '') {
|
||||
async fetchItems(query: any = '') {
|
||||
const params = typeof query === 'string' ? { search: query } : query;
|
||||
|
||||
const { results } = await makeRequest(this.path, { params });
|
||||
const {
|
||||
data: { results },
|
||||
} = await onCallApi().GET('/alert_receive_channels/', { params });
|
||||
|
||||
runInAction(() => {
|
||||
this.items = {
|
||||
...this.items,
|
||||
...results.reduce(
|
||||
(acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({
|
||||
(acc: { [key: number]: ApiSchemas['AlertReceiveChannel'] }, item: ApiSchemas['AlertReceiveChannel']) => ({
|
||||
...acc,
|
||||
[item.id]: omit(item, 'heartbeat'),
|
||||
}),
|
||||
|
|
@ -131,26 +116,28 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
this.populateHearbeats(results);
|
||||
|
||||
runInAction(() => {
|
||||
this.searchResult = results.map((item: AlertReceiveChannel) => item.id);
|
||||
this.searchResult = results.map((item: ApiSchemas['AlertReceiveChannel']) => item.id);
|
||||
});
|
||||
|
||||
this.updateCounters();
|
||||
this.fetchCounters();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async updatePaginatedItems({
|
||||
async fetchPaginatedItems({
|
||||
filters,
|
||||
page = 1,
|
||||
updateCounters = false,
|
||||
shouldFetchCounters = false,
|
||||
invalidateFn = undefined,
|
||||
}: {
|
||||
filters: SupportedIntegrationFilters;
|
||||
filters: operations['alert_receive_channels_list']['parameters']['query'];
|
||||
page: number;
|
||||
updateCounters: boolean;
|
||||
shouldFetchCounters: boolean;
|
||||
invalidateFn: () => boolean;
|
||||
}) {
|
||||
const { count, results, page_size } = await makeRequest(this.path, { params: { ...filters, page } });
|
||||
const {
|
||||
data: { count, results, page_size },
|
||||
} = await onCallApi().GET('/alert_receive_channels/', { params: { query: { ...filters, page } } });
|
||||
|
||||
if (invalidateFn?.()) {
|
||||
return undefined;
|
||||
|
|
@ -160,7 +147,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
this.items = {
|
||||
...this.items,
|
||||
...results.reduce(
|
||||
(acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({
|
||||
(acc: { [key: number]: ApiSchemas['AlertReceiveChannel'] }, item: ApiSchemas['AlertReceiveChannel']) => ({
|
||||
...acc,
|
||||
[item.id]: omit(item, 'heartbeat'),
|
||||
}),
|
||||
|
|
@ -174,26 +161,29 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
runInAction(() => {
|
||||
this.paginatedSearchResult = {
|
||||
count,
|
||||
results: results.map((item: AlertReceiveChannel) => item.id),
|
||||
results: results.map((item: ApiSchemas['AlertReceiveChannel']) => item.id),
|
||||
page_size,
|
||||
};
|
||||
});
|
||||
|
||||
if (updateCounters) {
|
||||
this.updateCounters();
|
||||
if (shouldFetchCounters) {
|
||||
this.fetchCounters();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
populateHearbeats(alertReceiveChannels: AlertReceiveChannel[]) {
|
||||
const heartbeats = alertReceiveChannels.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat;
|
||||
}
|
||||
populateHearbeats(alertReceiveChannels: Array<ApiSchemas['AlertReceiveChannelPolymorphic']>) {
|
||||
const heartbeats = alertReceiveChannels.reduce(
|
||||
(acc: any, alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.rootStore.heartbeatStore.items = {
|
||||
|
|
@ -203,7 +193,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
|
||||
const alertReceiveChannelToHeartbeat = alertReceiveChannels.reduce(
|
||||
(acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
(acc: any, alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
|
||||
}
|
||||
|
|
@ -221,8 +211,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateChannelFilters(alertReceiveChannelId: AlertReceiveChannel['id'], isOverwrite = false) {
|
||||
async fetchChannelFilters(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], isOverwrite = false) {
|
||||
const response = await makeRequest(`/channel_filters/`, {
|
||||
params: { alert_receive_channel: alertReceiveChannelId },
|
||||
});
|
||||
|
|
@ -259,32 +248,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateChannelFilter(channelFilterId: ChannelFilter['id']) {
|
||||
const response = await makeRequest(`/channel_filters/${channelFilterId}/`, {});
|
||||
|
||||
runInAction(() => {
|
||||
this.channelFilters = {
|
||||
...this.channelFilters,
|
||||
[channelFilterId]: response,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async migrateChannel(id: AlertReceiveChannel['id']) {
|
||||
return await makeRequest(`/alert_receive_channels/${id}/migrate`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
async createChannelFilter(data: Partial<ChannelFilter>) {
|
||||
return await makeRequest('/channel_filters/', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async saveChannelFilter(channelFilterId: ChannelFilter['id'], data: Partial<ChannelFilter>) {
|
||||
const response = await makeRequest(`/channel_filters/${channelFilterId}/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -301,9 +264,8 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action
|
||||
async moveChannelFilterToPosition(
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'],
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'],
|
||||
oldIndex: number,
|
||||
newIndex: number
|
||||
) {
|
||||
|
|
@ -317,10 +279,9 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
|
||||
await makeRequest(`/channel_filters/${channelFilterId}/move_to_position/?position=${newIndex}`, { method: 'PUT' });
|
||||
|
||||
this.updateChannelFilters(alertReceiveChannelId, true);
|
||||
this.fetchChannelFilters(alertReceiveChannelId, true);
|
||||
}
|
||||
|
||||
@action
|
||||
async deleteChannelFilter(channelFilterId: ChannelFilter['id']) {
|
||||
const channelFilter = this.channelFilters[channelFilterId];
|
||||
|
||||
|
|
@ -333,47 +294,43 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
method: 'DELETE',
|
||||
});
|
||||
|
||||
return this.updateChannelFilters(channelFilter.alert_receive_channel, true);
|
||||
return this.fetchChannelFilters(channelFilter.alert_receive_channel, true);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async updateAlertReceiveChannelOptions() {
|
||||
const response = await makeRequest(`/alert_receive_channels/integration_options/`, {});
|
||||
async fetchAlertReceiveChannelOptions() {
|
||||
const { data } = await onCallApi().GET(`/alert_receive_channels/integration_options/`, undefined);
|
||||
|
||||
runInAction(() => {
|
||||
this.alertReceiveChannelOptions = response;
|
||||
this.alertReceiveChannelOptions = data;
|
||||
});
|
||||
}
|
||||
|
||||
getIntegration(alertReceiveChannel: Partial<AlertReceiveChannel>): SelectOption {
|
||||
return (
|
||||
this.alertReceiveChannelOptions &&
|
||||
alertReceiveChannel &&
|
||||
this.alertReceiveChannelOptions.find(
|
||||
(alertReceiveChannelOption: SelectOption) => alertReceiveChannelOption.value === alertReceiveChannel.integration
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@WithGlobalNotification({ success: 'Integration has been saved', failure: 'Failed to save integration' })
|
||||
async saveAlertReceiveChannel(id: AlertReceiveChannel['id'], data: Partial<AlertReceiveChannel>) {
|
||||
const item = await this.update(id, data, undefined, true);
|
||||
async saveAlertReceiveChannel(
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'],
|
||||
payload: OmitReadonlyMembers<ApiSchemas['AlertReceiveChannelUpdate']>
|
||||
) {
|
||||
const currentIntegration = this.items[id];
|
||||
const { data } = await onCallApi().PUT('/alert_receive_channels/{id}/', {
|
||||
params: { path: { id } },
|
||||
body: {
|
||||
description_short: currentIntegration.description_short,
|
||||
verbal_name: currentIntegration.verbal_name,
|
||||
allow_source_based_resolving: currentIntegration.allow_source_based_resolving,
|
||||
alert_group_labels: currentIntegration.alert_group_labels,
|
||||
...payload,
|
||||
} as ApiSchemas['AlertReceiveChannelUpdate'],
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.items = {
|
||||
...this.items,
|
||||
[id]: item,
|
||||
[id]: data,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAlertReceiveChannel(id: AlertReceiveChannel['id']) {
|
||||
return await this.delete(id);
|
||||
}
|
||||
|
||||
@action
|
||||
async updateTemplates(alertReceiveChannelId: AlertReceiveChannel['id'], alertGroupId?: Alert['pk']) {
|
||||
async fetchTemplates(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], alertGroupId?: Alert['pk']) {
|
||||
const response = await makeRequest(`/alert_receive_channel_templates/${alertReceiveChannelId}/`, {
|
||||
params: { alert_group_id: alertGroupId },
|
||||
withCredentials: true,
|
||||
|
|
@ -387,20 +344,10 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateItem(id: AlertReceiveChannel['id']) {
|
||||
const item = await this.getById(id);
|
||||
|
||||
runInAction(() => {
|
||||
this.items = {
|
||||
...this.items,
|
||||
[id]: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async saveTemplates(alertReceiveChannelId: AlertReceiveChannel['id'], data: Partial<AlertTemplatesDTO>) {
|
||||
async saveTemplates(
|
||||
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'],
|
||||
data: Partial<AlertTemplatesDTO>
|
||||
) {
|
||||
const response = await makeRequest(`/alert_receive_channel_templates/${alertReceiveChannelId}/`, {
|
||||
method: 'PUT',
|
||||
data,
|
||||
|
|
@ -415,26 +362,23 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
async getGrafanaAlertingContactPoints() {
|
||||
return await makeRequest(`${this.path}contact_points/`, {}).catch(showApiError);
|
||||
}
|
||||
|
||||
@action
|
||||
async updateConnectedContactPoints(alertReceiveChannelId: AlertReceiveChannel['id']) {
|
||||
const response = await makeRequest(`${this.path}${alertReceiveChannelId}/connected_contact_points `, {});
|
||||
async fetchConnectedContactPoints(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']) {
|
||||
const { data } = await onCallApi().GET('/alert_receive_channels/{id}/connected_contact_points/', {
|
||||
params: { path: { id: alertReceiveChannelId } },
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.connectedContactPoints = {
|
||||
...this.connectedContactPoints,
|
||||
|
||||
[alertReceiveChannelId]: response.reduce((list: ContactPoint[], payload) => {
|
||||
payload.contact_points.forEach((contactPoint: { name: string; notification_connected: boolean }) => {
|
||||
[alertReceiveChannelId]: data.reduce((list: ContactPoint[], payload) => {
|
||||
payload.contact_points.forEach((contactPoint) => {
|
||||
list.push({
|
||||
dataSourceName: payload.name,
|
||||
dataSourceId: payload.uid,
|
||||
contactPoint: contactPoint.name,
|
||||
notificationConnected: contactPoint.notification_connected,
|
||||
} as ContactPoint);
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
|
|
@ -443,140 +387,25 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
async connectContactPoint(
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'],
|
||||
datasource_uid: string,
|
||||
contact_point_name: string
|
||||
) {
|
||||
return await makeRequest(`${this.path}${alertReceiveChannelId}/connect_contact_point`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
datasource_uid,
|
||||
contact_point_name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async disconnectContactPoint(
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'],
|
||||
datasource_uid: string,
|
||||
contact_point_name: string
|
||||
) {
|
||||
return await makeRequest(`${this.path}${alertReceiveChannelId}/disconnect_contact_point`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
datasource_uid,
|
||||
contact_point_name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createContactPoint(
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'],
|
||||
datasource_uid: string,
|
||||
contact_point_name: string
|
||||
) {
|
||||
return await makeRequest(`${this.path}${alertReceiveChannelId}/create_contact_point`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
datasource_uid,
|
||||
contact_point_name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getAccessLogs(alertReceiveChannelId: AlertReceiveChannel['id']) {
|
||||
const { integration_log } = await makeRequest(`/alert_receive_channel_access_log/${alertReceiveChannelId}/`, {});
|
||||
|
||||
return integration_log;
|
||||
}
|
||||
|
||||
async sendDemoAlert(id: AlertReceiveChannel['id'], payload: string = undefined) {
|
||||
const requestConfig: any = {
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
if (payload) {
|
||||
requestConfig.data = {
|
||||
demo_alert_payload: payload,
|
||||
};
|
||||
}
|
||||
|
||||
await makeRequest(`${this.path}${id}/send_demo_alert/`, requestConfig).catch(showApiError);
|
||||
}
|
||||
|
||||
async sendDemoAlertToParticularRoute(id: ChannelFilter['id']) {
|
||||
await makeRequest(`/channel_filters/${id}/send_demo_alert/`, { method: 'POST' }).catch(showApiError);
|
||||
}
|
||||
|
||||
async convertRegexpTemplateToJinja2Template(id: ChannelFilter['id']) {
|
||||
const result = await makeRequest(`/channel_filters/${id}/convert_from_regex_to_jinja2/`, { method: 'POST' }).catch(
|
||||
showApiError
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
async renderPreview(id: AlertReceiveChannel['id'], template_name: string, template_body: string, payload: JSON) {
|
||||
return await makeRequest(`${this.path}${id}/preview_template/`, {
|
||||
method: 'POST',
|
||||
data: { template_name, template_body, payload },
|
||||
});
|
||||
}
|
||||
|
||||
async changeTeam(id: AlertReceiveChannel['id'], teamId: GrafanaTeam['id']) {
|
||||
return await makeRequest(`${this.path}${id}/change_team`, {
|
||||
params: { team_id: String(teamId) },
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateCounters() {
|
||||
const counters = await makeRequest(`${this.path}counters`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
async fetchCounters() {
|
||||
const { data } = await onCallApi().GET('/alert_receive_channels/counters/', undefined);
|
||||
runInAction(() => {
|
||||
this.counters = counters;
|
||||
this.counters = data;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateCountersForIntegration(id: AlertReceiveChannel['id']): Promise<any> {
|
||||
const counters = await makeRequest(`${this.path}${id}/counters`, {
|
||||
method: 'GET',
|
||||
});
|
||||
async fetchCountersForIntegration(id: ApiSchemas['AlertReceiveChannel']['id']) {
|
||||
const { data } = await onCallApi().GET('/alert_receive_channels/{id}/counters/', { params: { path: { id } } });
|
||||
|
||||
runInAction(() => {
|
||||
this.counters = {
|
||||
...this.counters,
|
||||
[id]: {
|
||||
...counters[id],
|
||||
...data[id],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return counters;
|
||||
return data;
|
||||
}
|
||||
|
||||
startMaintenanceMode = (id: AlertReceiveChannel['id'], mode: MaintenanceMode, duration: number): Promise<void> =>
|
||||
makeRequest<null>(`${this.path}${id}/start_maintenance/`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
mode,
|
||||
duration,
|
||||
},
|
||||
});
|
||||
|
||||
stopMaintenanceMode = (id: AlertReceiveChannel['id']) =>
|
||||
makeRequest<null>(`${this.path}${id}/stop_maintenance/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
addLabel = (id: AlertReceiveChannel['id'], data) => {
|
||||
makeRequest(`${this.path}${id}/associate_label`, {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,12 @@
|
|||
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
|
||||
import { Heartbeat } from 'models/heartbeat/heartbeat.types';
|
||||
import { LabelKeyValue } from 'models/label/label.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { operations } from 'network/oncall-api/autogenerated-api.types';
|
||||
|
||||
export enum MaintenanceMode {
|
||||
Debug = 0,
|
||||
Maintenance = 1,
|
||||
}
|
||||
|
||||
export interface AlertReceiveChannelOption {
|
||||
display_name: string;
|
||||
value: string;
|
||||
featured: boolean;
|
||||
short_description: string;
|
||||
featured_tag_name: string;
|
||||
}
|
||||
|
||||
export interface AlertReceiveChannelCounters {
|
||||
alerts_count: number;
|
||||
alert_groups_count: number;
|
||||
}
|
||||
|
||||
export interface AlertReceiveChannel {
|
||||
id: string;
|
||||
integration: string;
|
||||
smile_code: string;
|
||||
verbal_name: string;
|
||||
description: string;
|
||||
description_short: string;
|
||||
author: User['pk'];
|
||||
team: GrafanaTeam['id'];
|
||||
created_at: string;
|
||||
integration_url: string;
|
||||
inbound_email: string;
|
||||
allow_source_based_resolving: boolean;
|
||||
is_able_to_autoresolve: boolean;
|
||||
is_based_on_alertmanager: boolean;
|
||||
default_channel_filter: number;
|
||||
instructions: string;
|
||||
demo_alert_enabled: boolean;
|
||||
demo_alert_payload: any;
|
||||
maintenance_mode?: MaintenanceMode;
|
||||
maintenance_till?: number;
|
||||
heartbeat: Heartbeat | null;
|
||||
is_available_for_integration_heartbeat: boolean;
|
||||
routes_count: number;
|
||||
connected_escalations_chains_count: number;
|
||||
allow_delete: boolean;
|
||||
deleted?: boolean;
|
||||
labels: LabelKeyValue[];
|
||||
alert_group_labels: {
|
||||
inheritable: Record<LabelKeyValue['key']['id'], boolean>;
|
||||
custom: LabelKeyValue[];
|
||||
template: string;
|
||||
};
|
||||
alertmanager_v2_migrated_at?: string | null;
|
||||
}
|
||||
|
||||
export interface AlertReceiveChannelChoice {
|
||||
display_name: string;
|
||||
value: number;
|
||||
}
|
||||
export type AlertReceiveChannelCounters =
|
||||
operations['alert_receive_channels_counters_retrieve']['responses']['200']['content']['application/json'][string];
|
||||
|
||||
export interface ContactPoint {
|
||||
dataSourceName: string;
|
||||
|
|
@ -68,11 +14,3 @@ export interface ContactPoint {
|
|||
contactPoint: string;
|
||||
notificationConnected: boolean;
|
||||
}
|
||||
|
||||
export interface SupportedIntegrationFilters {
|
||||
integration?: string[];
|
||||
integration_ne?: string[];
|
||||
team?: string[];
|
||||
label?: string[];
|
||||
searchTerm?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,15 +20,15 @@ export class AlertReceiveChannelFiltersStore extends BaseStore {
|
|||
this.path = '/alert_receive_channels/';
|
||||
}
|
||||
|
||||
getSearchResult() {
|
||||
getSearchResult = () => {
|
||||
if (!this.searchResult) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.searchResult.map((value: SelectOption['value']) => this.items?.[value]);
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(query = '') {
|
||||
const results = await makeRequest(`${this.path}`, {
|
||||
params: { search: query, filters: true },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { action, observable, makeObservable, runInAction } from 'mobx';
|
||||
import qs from 'query-string';
|
||||
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { BaseStore } from 'models/base_store';
|
||||
import { ActionKey } from 'models/loader/action-keys';
|
||||
import { User } from 'models/user/user.types';
|
||||
|
|
@ -97,27 +96,58 @@ export class AlertGroupStore extends BaseStore {
|
|||
}).catch(showApiError);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItem(id: Alert['pk']) {
|
||||
const item = await this.getById(id);
|
||||
|
||||
runInAction(() => {
|
||||
this.items = {
|
||||
...this.items,
|
||||
[item.id]: item,
|
||||
[item.pk]: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
@action.bound
|
||||
async fetchItems(query = '', params = {}) {
|
||||
const { results } = await makeRequest(`${this.path}`, {
|
||||
params: { search: query, ...params },
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.items = {
|
||||
...this.items,
|
||||
...results.reduce(
|
||||
(acc: { [key: number]: Alert }, item: Alert) => ({
|
||||
...acc,
|
||||
[item.pk]: item,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
};
|
||||
|
||||
this.searchResult = {
|
||||
...this.searchResult,
|
||||
[query]: results.map((item: Alert) => item.pk),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async fetchItemsAvailableForAttachment(query: string) {
|
||||
await this.fetchItems(query, {
|
||||
status: [IncidentStatus.Acknowledged, IncidentStatus.Firing, IncidentStatus.Silenced],
|
||||
});
|
||||
}
|
||||
|
||||
getSearchResult = (query = '') => {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.searchResult[query].map((id: Alert['pk']) => this.items[id]);
|
||||
}
|
||||
};
|
||||
|
||||
async getAlertGroupsForIntegration(integrationId: AlertReceiveChannel['id']) {
|
||||
async getAlertGroupsForIntegration(integrationId: ApiSchemas['AlertReceiveChannel']['id']) {
|
||||
const { results } = await makeRequest(`${this.path}`, {
|
||||
params: { integration: integrationId },
|
||||
});
|
||||
|
|
@ -202,7 +232,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
await this.fetchTableSettings();
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateBulkActions() {
|
||||
const response = await makeRequest(`${this.path}bulk_action_options/`, {});
|
||||
|
||||
|
|
@ -242,12 +272,12 @@ export class AlertGroupStore extends BaseStore {
|
|||
this.setLiveUpdatesPaused(false);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
setLiveUpdatesPaused(value: boolean) {
|
||||
this.liveUpdatesPaused = value;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
@AutoLoadingState(ActionKey.UPDATE_FILTERS_AND_FETCH_INCIDENTS)
|
||||
async updateIncidentFiltersAndRefetchIncidentsAndStats(params: any, keepCursor = false) {
|
||||
if (!keepCursor) {
|
||||
|
|
@ -257,21 +287,21 @@ export class AlertGroupStore extends BaseStore {
|
|||
await this.fetchIncidentsAndStats();
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateIncidentsCursor(cursor: string) {
|
||||
this.setIncidentsCursor(cursor);
|
||||
|
||||
this.fetchAlertGroups();
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async setIncidentsCursor(cursor: string) {
|
||||
this.incidentsCursor = cursor;
|
||||
|
||||
LocationHelper.update({ cursor }, 'partial');
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async setIncidentsItemsPerPage() {
|
||||
this.setIncidentsCursor(undefined);
|
||||
|
||||
|
|
@ -360,7 +390,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
return await makeRequest(`/alerts/${pk}`, {});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async getNewIncidentsStats() {
|
||||
const result = await makeRequest(`${this.path}stats/`, {
|
||||
params: {
|
||||
|
|
@ -374,7 +404,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async getAcknowledgedIncidentsStats() {
|
||||
const result = await makeRequest(`${this.path}stats/`, {
|
||||
params: {
|
||||
|
|
@ -388,7 +418,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async getResolvedIncidentsStats() {
|
||||
const result = await makeRequest(`${this.path}stats/`, {
|
||||
params: {
|
||||
|
|
@ -402,7 +432,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async getSilencedIncidentsStats() {
|
||||
const result = await makeRequest(`${this.path}stats/`, {
|
||||
params: {
|
||||
|
|
@ -416,7 +446,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async doIncidentAction(alertId: Alert['pk'], action: AlertAction, isUndo = false, data?: any) {
|
||||
this.updateAlert(alertId, { loading: true });
|
||||
|
||||
|
|
@ -463,7 +493,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateAlert(pk: Alert['pk'], value: Partial<Alert>) {
|
||||
this.alerts.set(pk, {
|
||||
...(this.alerts.get(pk) as Alert),
|
||||
|
|
@ -478,7 +508,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
}).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async fetchTableSettings(): Promise<void> {
|
||||
const tableSettings = await makeRequest('/alertgroup_table_settings', {});
|
||||
|
||||
|
|
@ -493,7 +523,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
@AutoLoadingState(ActionKey.ADD_NEW_COLUMN_TO_ALERT_GROUP)
|
||||
async updateTableSettings(
|
||||
columns: { visible: AlertGroupColumn[]; hidden: AlertGroupColumn[] },
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Channel } from 'models/channel/channel';
|
||||
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
|
||||
import { LabelKeyValue } from 'models/label/label.types';
|
||||
import { PagedUser, User } from 'models/user/user.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
||||
export enum IncidentStatus {
|
||||
'Firing',
|
||||
|
|
@ -79,7 +79,7 @@ export interface Alert {
|
|||
status: IncidentStatus;
|
||||
short?: boolean;
|
||||
root_alert_group?: Alert;
|
||||
alert_receive_channel: Partial<AlertReceiveChannel>;
|
||||
alert_receive_channel: Partial<ApiSchemas['FastAlertReceiveChannel']>;
|
||||
paged_users: PagedUser[];
|
||||
team: GrafanaTeam['id'];
|
||||
grafana_incident_id: string | null;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class ApiTokenStore extends BaseStore {
|
|||
this.path = '/tokens/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(query = '') {
|
||||
const results = await makeRequest(`${this.path}`, {
|
||||
params: { search: query },
|
||||
|
|
@ -46,13 +46,13 @@ export class ApiTokenStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
getSearchResult = (query = '') => {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.searchResult[query].map((apiTokenId: ApiToken['id']) => this.items[apiTokenId]);
|
||||
}
|
||||
};
|
||||
|
||||
async revokeApiToken(id: ApiToken['id']) {
|
||||
return await makeRequest(`${this.path}${id}/`, {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export class BaseStore {
|
|||
throw error;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async getAll(query = '') {
|
||||
return await makeRequest(`${this.path}`, {
|
||||
params: { search: query },
|
||||
|
|
@ -50,7 +50,7 @@ export class BaseStore {
|
|||
}).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async getById(id: string, skipErrorHandling = false, fromOrganization = false) {
|
||||
return await makeRequest(`${this.path}${id}`, {
|
||||
method: 'GET',
|
||||
|
|
@ -58,7 +58,7 @@ export class BaseStore {
|
|||
}).catch((error) => this.onApiError(error, skipErrorHandling));
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async create<RT = any>(data: any, skipErrorHandling = false): Promise<RT | void> {
|
||||
return await makeRequest<RT>(this.path, {
|
||||
method: 'POST',
|
||||
|
|
@ -68,7 +68,7 @@ export class BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async update<RT = any>(id: any, data: any, params: any = null, skipErrorHandling = false): Promise<RT | void> {
|
||||
const result = await makeRequest<RT>(`${this.path}${id}/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -83,7 +83,7 @@ export class BaseStore {
|
|||
return result;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async delete(id: any) {
|
||||
const result = await makeRequest(`${this.path}${id}/`, {
|
||||
method: 'DELETE',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
||||
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
|
||||
import { TelegramChannel, TelegramChannelDetails } from 'models/telegram_channel/telegram_channel.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
||||
export enum FilteringTermType {
|
||||
regex,
|
||||
|
|
@ -10,7 +10,7 @@ export enum FilteringTermType {
|
|||
|
||||
export interface ChannelFilter {
|
||||
id: string;
|
||||
alert_receive_channel: AlertReceiveChannel['id'];
|
||||
alert_receive_channel: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
slack_channel_id?: SlackChannel['id'];
|
||||
slack_channel?: SlackChannel;
|
||||
telegram_channel?: TelegramChannel['id'];
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class CloudStore extends BaseStore {
|
|||
this.path = '/cloud_users/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(page = 1) {
|
||||
const { matched_users_count, results } = await makeRequest(this.path, {
|
||||
params: { page },
|
||||
|
|
@ -49,12 +49,12 @@ export class CloudStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
getSearchResult() {
|
||||
getSearchResult = () => {
|
||||
return {
|
||||
matched_users_count: this.searchResult.matched_users_count,
|
||||
results: this.searchResult.results && this.searchResult.results.map((id: Cloud['id']) => this.items?.[id]),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
async syncCloudUsers() {
|
||||
return await makeRequest(`${this.path}`, { method: 'POST' });
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export class DirectPagingStore extends BaseStore {
|
|||
this.path = '/direct_paging/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
addUserToSelectedUsers = (user: UserCurrentlyOnCall) => {
|
||||
this.selectedUserResponders = [
|
||||
...this.selectedUserResponders,
|
||||
|
|
@ -40,22 +40,22 @@ export class DirectPagingStore extends BaseStore {
|
|||
];
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
resetSelectedUsers = () => {
|
||||
this.selectedUserResponders = [];
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
updateSelectedTeam = (team: GrafanaTeam) => {
|
||||
this.selectedTeamResponder = team;
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
resetSelectedTeam = () => {
|
||||
this.selectedTeamResponder = null;
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
removeSelectedUser(index: number) {
|
||||
this.selectedUserResponders = [
|
||||
...this.selectedUserResponders.slice(0, index),
|
||||
|
|
@ -63,7 +63,7 @@ export class DirectPagingStore extends BaseStore {
|
|||
];
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
updateSelectedUserImportantStatus(index: number, important: boolean) {
|
||||
this.selectedUserResponders = [
|
||||
...this.selectedUserResponders.slice(0, index),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
this.path = '/escalation_chains/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async loadItem(id: EscalationChain['id'], skipErrorHandling = false): Promise<EscalationChain> {
|
||||
const escalationChain = await this.getById(id, skipErrorHandling);
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
return escalationChain;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateById(id: EscalationChain['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async save(id: EscalationChain['id'], data: Partial<EscalationChain>) {
|
||||
const response = await super.update(id, data);
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateEscalationChainDetails(id: EscalationChain['id']) {
|
||||
const response = await makeRequest(`${this.path}${id}/details/`, {});
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItem(id: EscalationChain['id'], skipErrorHandling = false): Promise<EscalationChain> {
|
||||
let escalationChain;
|
||||
try {
|
||||
|
|
@ -107,7 +107,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
return escalationChain;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(query: any = '') {
|
||||
const params = typeof query === 'string' ? { search: query } : query;
|
||||
|
||||
|
|
@ -140,13 +140,13 @@ export class EscalationChainStore extends BaseStore {
|
|||
this.loading = false;
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
getSearchResult = (query = '') => {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.searchResult[query].map((escalationChainId: EscalationChain['id']) => this.items[escalationChainId]);
|
||||
}
|
||||
};
|
||||
|
||||
clone = (escalationChainId: EscalationChain['id'], data: Partial<EscalationChain>): Promise<EscalationChain> =>
|
||||
makeRequest<EscalationChain>(`${this.path}${escalationChainId}/copy/`, {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateEscalationPolicies(escalationChainId: EscalationChain['id']) {
|
||||
const response = await makeRequest(this.path, {
|
||||
params: { escalation_chain: escalationChainId },
|
||||
|
|
@ -91,7 +91,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
createEscalationPolicy(escalationChainId: EscalationChain['id'], data: Partial<EscalationPolicy>) {
|
||||
return super.create({
|
||||
...data,
|
||||
|
|
@ -99,7 +99,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async saveEscalationPolicy(id: EscalationPolicy['id'], data: Partial<EscalationPolicy>) {
|
||||
this.items[id] = {
|
||||
...this.items[id],
|
||||
|
|
@ -113,7 +113,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async moveEscalationPolicyToPosition(oldIndex: any, newIndex: any, escalationChainId: EscalationChain['id']) {
|
||||
const escalationPolicyId = this.escalationChainToEscalationPolicy[escalationChainId][oldIndex];
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
this.updateEscalationPolicies(escalationChainId);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async deleteEscalationPolicy(data: Partial<EscalationPolicy>) {
|
||||
const index = this.escalationChainToEscalationPolicy[data.escalation_chain].findIndex(
|
||||
(escalationPolicyId: EscalationPolicy['id']) => escalationPolicyId === data.id
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class FiltersStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
setNeedToParseFilters(value: boolean) {
|
||||
this.needToParseFilters = value;
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ export class FiltersStore extends BaseStore {
|
|||
return this._globalValues;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
public async updateOptionsForPage(page: string) {
|
||||
const result = await makeRequest(`/${getApiPathByPage(page)}/filters/`, {});
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ export class FiltersStore extends BaseStore {
|
|||
return result;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
updateValuesForPage(page: string, value: FiltersValues) {
|
||||
this.values = {
|
||||
...this.values,
|
||||
|
|
@ -81,12 +81,12 @@ export class FiltersStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
setCurrentTablePageNum(page: PAGE, currentTablePageNum: number) {
|
||||
this.currentTablePageNum[page] = currentTablePageNum;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
applyLabelFilter = (label: LabelKeyValue, page: PAGE) => {
|
||||
const currentLabelFilterValues = this.values[page]?.label || [];
|
||||
const labelToAddString = `${label.key.id}:${label.value.id}`;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export class GlobalSettingStore extends BaseStore {
|
|||
this.path = '/live_settings/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateById(id: GlobalSetting['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export class GlobalSettingStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(query = '') {
|
||||
const results = await this.getAll();
|
||||
|
||||
|
|
@ -55,13 +55,13 @@ export class GlobalSettingStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
getSearchResult = (query = '') => {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.searchResult[query].map((globalSettingId: GlobalSetting['id']) => this.items[globalSettingId]);
|
||||
}
|
||||
};
|
||||
|
||||
async getGlobalSettingItemByName(name: string) {
|
||||
const results = await this.getAll();
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class GrafanaTeamStore extends BaseStore {
|
|||
this.path = '/teams/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateTeam(id: GrafanaTeam['id'], data: Partial<GrafanaTeam>) {
|
||||
const result = await this.update(id, data);
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ export class GrafanaTeamStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
getSearchResult() {
|
||||
getSearchResult = () => {
|
||||
return this.searchResult.map((teamId: GrafanaTeam['id']) => this.items[teamId]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { action, observable, makeObservable, runInAction } from 'mobx';
|
||||
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { BaseStore } from 'models/base_store';
|
||||
import { makeRequest } from 'network/network';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { RootStore } from 'state/rootStore';
|
||||
|
||||
import { Heartbeat } from './heartbeat.types';
|
||||
|
|
@ -22,7 +22,7 @@ export class HeartbeatStore extends BaseStore {
|
|||
this.path = '/heartbeats/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateTimeoutOptions() {
|
||||
const result = await makeRequest(`${this.path}timeout_options/`, {});
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ export class HeartbeatStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async saveHeartbeat(id: Heartbeat['id'], data: Partial<Heartbeat>) {
|
||||
const response = await super.update<Heartbeat>(id, data);
|
||||
|
||||
|
|
@ -47,8 +47,8 @@ export class HeartbeatStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async createHeartbeat(alertReceiveChannelId: AlertReceiveChannel['id'], data: Partial<Heartbeat>) {
|
||||
@action.bound
|
||||
async createHeartbeat(alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'], data: Partial<Heartbeat>) {
|
||||
const response = await super.create<Heartbeat>({
|
||||
alert_receive_channel: alertReceiveChannelId,
|
||||
...data,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
||||
export interface Heartbeat {
|
||||
id: string;
|
||||
last_heartbeat_time_verbal: string;
|
||||
alert_receive_channel: AlertReceiveChannel['id'];
|
||||
alert_receive_channel: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
link: string;
|
||||
timeout_seconds: number;
|
||||
status: boolean;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { action, makeObservable } from 'mobx';
|
|||
import { BaseStore } from 'models/base_store';
|
||||
import { makeRequest } from 'network/network';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import onCallApi from 'network/oncall-api/http-client';
|
||||
import { onCallApi } from 'network/oncall-api/http-client';
|
||||
import { RootStore } from 'state/rootStore';
|
||||
import { WithGlobalNotification } from 'utils/decorators';
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ export class LabelStore extends BaseStore {
|
|||
|
||||
@action.bound
|
||||
public async loadKeys(search = '') {
|
||||
const { data } = await onCallApi.GET('/labels/keys/', undefined);
|
||||
const { data } = await onCallApi().GET('/labels/keys/', undefined);
|
||||
|
||||
const filtered = data.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ class LoaderStoreClass {
|
|||
this.items[actionKey] = isLoading;
|
||||
}
|
||||
}
|
||||
|
||||
isLoading(actionKey: string): boolean {
|
||||
return !!this.items[actionKey];
|
||||
}
|
||||
}
|
||||
|
||||
export const LoaderStore = new LoaderStoreClass();
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class MSTeamsChannelStore extends BaseStore {
|
|||
this.path = '/msteams/channels/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateMSTeamsChannels() {
|
||||
const response = await makeRequest(this.path, {});
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ export class MSTeamsChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateById(id: MSTeamsChannel['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ export class MSTeamsChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(query = '') {
|
||||
const result = await this.getAll();
|
||||
|
||||
|
|
@ -83,12 +83,12 @@ export class MSTeamsChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
getSearchResult = (query = '') => {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
return this.searchResult[query].map((msteamsChannelId: MSTeamsChannel['id']) => this.items[msteamsChannelId]);
|
||||
}
|
||||
};
|
||||
|
||||
@computed
|
||||
get hasItems() {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
this.path = '/webhooks/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async loadItem(id: OutgoingWebhook['id'], skipErrorHandling = false): Promise<OutgoingWebhook> {
|
||||
const outgoingWebhook = await this.getById(id, skipErrorHandling);
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
return outgoingWebhook;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateById(id: OutgoingWebhook['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItem(id: OutgoingWebhook['id'], fromOrganization = false) {
|
||||
const response = await this.getById(id, false, fromOrganization);
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(query: any = '') {
|
||||
const params = typeof query === 'string' ? { search: query } : query;
|
||||
|
||||
|
|
@ -95,13 +95,13 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
getSearchResult = (query = '') => {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.searchResult[query].map((outgoingWebhookId: OutgoingWebhook['id']) => this.items[outgoingWebhookId]);
|
||||
}
|
||||
};
|
||||
|
||||
async getLastResponses(id: OutgoingWebhook['id']) {
|
||||
const result = await makeRequest(`${this.path}${id}/responses`, {});
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export class ScheduleStore extends BaseStore {
|
|||
this.path = '/schedules/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async loadItem(id: Schedule['id'], skipErrorHandling = false): Promise<Schedule> {
|
||||
const schedule = await this.getById(id, skipErrorHandling);
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return schedule;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(
|
||||
f: RemoteFiltersType | string = { searchTerm: '', type: undefined, used: undefined },
|
||||
page = 1,
|
||||
|
|
@ -182,7 +182,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItem(id: Schedule['id'], fromOrganization = false) {
|
||||
if (id) {
|
||||
let schedule;
|
||||
|
|
@ -211,13 +211,13 @@ export class ScheduleStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
getSearchResult() {
|
||||
getSearchResult = () => {
|
||||
return {
|
||||
page_size: this.searchResult.page_size,
|
||||
count: this.searchResult.count,
|
||||
results: this.searchResult.results?.map((scheduleId: Schedule['id']) => this.items[scheduleId]),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@action.bound
|
||||
async getScoreQuality(scheduleId: Schedule['id']) {
|
||||
|
|
@ -256,7 +256,7 @@ export class ScheduleStore extends BaseStore {
|
|||
|
||||
// ------- NEW SCHEDULES API ENDPOINTS ---------
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async createRotation(scheduleId: Schedule['id'], isOverride: boolean, params: Partial<Shift>) {
|
||||
const type = isOverride ? 3 : 2;
|
||||
|
||||
|
|
@ -324,7 +324,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateShiftsSwapPreview(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, params: Partial<ShiftSwap>) {
|
||||
const fromString = getFromString(startMoment);
|
||||
|
||||
|
|
@ -350,7 +350,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
clearPreview() {
|
||||
this.finalPreview = undefined;
|
||||
this.rotationPreview = undefined;
|
||||
|
|
@ -359,7 +359,7 @@ export class ScheduleStore extends BaseStore {
|
|||
this.rotationFormLiveParams = undefined;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateRotation(shiftId: Shift['id'], params: Partial<Shift>) {
|
||||
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
|
||||
params: { force: true },
|
||||
|
|
@ -377,7 +377,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateRotationAsNew(shiftId: Shift['id'], params: Partial<Shift>) {
|
||||
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
|
||||
data: { ...params },
|
||||
|
|
@ -394,7 +394,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
updateRelatedEscalationChains = async (id: Schedule['id']) => {
|
||||
const response = await makeRequest(`/schedules/${id}/related_escalation_chains`, {
|
||||
method: 'GET',
|
||||
|
|
@ -410,7 +410,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
updateRelatedUsers = async (id: Schedule['id']) => {
|
||||
const { users } = await makeRequest(`/schedules/${id}/next_shifts_per_user`, {
|
||||
method: 'GET',
|
||||
|
|
@ -426,7 +426,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return users;
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateOncallShifts(scheduleId: Schedule['id']) {
|
||||
const { results } = await makeRequest(`/oncall_shifts/`, {
|
||||
params: {
|
||||
|
|
@ -449,7 +449,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateOncallShift(shiftId: Shift['id']) {
|
||||
if (this.shiftsCurrentlyUpdating[shiftId]) {
|
||||
return;
|
||||
|
|
@ -471,7 +471,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async saveOncallShift(shiftId: Shift['id'], data: Partial<Shift>) {
|
||||
const response = await makeRequest(`/oncall_shifts/${shiftId}`, { method: 'PUT', data });
|
||||
|
||||
|
|
@ -492,7 +492,7 @@ export class ScheduleStore extends BaseStore {
|
|||
}).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateEvents(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, type: RotationType = 'rotation', days = 9) {
|
||||
const dayBefore = startMoment.subtract(1, 'day');
|
||||
|
||||
|
|
@ -554,7 +554,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateDaysOptions() {
|
||||
const result = await makeRequest(`/oncall_shifts/days_options/`, {
|
||||
method: 'GET',
|
||||
|
|
@ -577,7 +577,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return await makeRequest(`/shift_swaps/${shiftSwapId}/take`, { method: 'POST' }).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async loadShiftSwap(id: ShiftSwap['id']) {
|
||||
const result = await makeRequest(`/shift_swaps/${id}`, { params: { expand_users: true } });
|
||||
|
||||
|
|
@ -588,7 +588,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return result;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateShiftSwaps(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, days = 9) {
|
||||
const fromString = getFromString(startMoment);
|
||||
|
||||
|
|
@ -630,7 +630,7 @@ export class ScheduleStore extends BaseStore {
|
|||
}
|
||||
|
||||
@AutoLoadingState(ActionKey.UPDATE_PERSONAL_EVENTS)
|
||||
@action
|
||||
@action.bound
|
||||
async updatePersonalEvents(userPk: User['pk'], startMoment: dayjs.Dayjs, days = 9, isUpdateOnCallNow = false) {
|
||||
const fromString = getFromString(startMoment);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export class SlackStore extends BaseStore {
|
|||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateSlackSettings() {
|
||||
const result = await makeRequest('/slack_settings/', {});
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export class SlackStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async saveSlackSettings(data: Partial<SlackSettings>) {
|
||||
const result = await makeRequest('/slack_settings/', {
|
||||
data,
|
||||
|
|
@ -40,7 +40,7 @@ export class SlackStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async setGeneralLogChannelId(id: SlackChannel['id']) {
|
||||
return await makeRequest('/set_general_channel/', {
|
||||
method: 'POST',
|
||||
|
|
@ -48,7 +48,7 @@ export class SlackStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateSlackIntegrationData(slack_id: string) {
|
||||
const result = await makeRequest('/slack_integration/', {
|
||||
params: { slack_id },
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class SlackChannelStore extends BaseStore {
|
|||
this.path = '/slack_channels/';
|
||||
}
|
||||
|
||||
@action // deprecated, use updateItem instead
|
||||
@action.bound // deprecated, use updateItem instead
|
||||
async updateById(id: SlackChannel['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ export class SlackChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItem(id: SlackChannel['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ export class SlackChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(query = '') {
|
||||
const { results } = await makeRequest(`${this.path}`, {
|
||||
params: { search: query },
|
||||
|
|
@ -70,11 +70,11 @@ export class SlackChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
getSearchResult = (query = '') => {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.searchResult[query].map((slackChannelId: SlackChannel['id']) => this.items[slackChannelId]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class TelegramChannelStore extends BaseStore {
|
|||
this.path = '/telegram_channels/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateTelegramChannels() {
|
||||
const response = await makeRequest(this.path, {});
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ export class TelegramChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateById(id: TelegramChannel['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ export class TelegramChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(query = '') {
|
||||
const result = await this.getAll();
|
||||
|
||||
|
|
@ -83,12 +83,12 @@ export class TelegramChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
getSearchResult = (query = '') => {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
return this.searchResult[query].map((telegramChannelId: TelegramChannel['id']) => this.items[telegramChannelId]);
|
||||
}
|
||||
};
|
||||
|
||||
@computed
|
||||
get hasItems() {
|
||||
|
|
@ -111,14 +111,14 @@ export class TelegramChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async makeTelegramChannelDefault(id: TelegramChannel['id']) {
|
||||
return makeRequest(`/telegram_channels/${id}/set_default/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async deleteTelegramChannel(id: TelegramChannel['id']) {
|
||||
return super.delete(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -595,8 +595,6 @@ export const allTimezones = [
|
|||
'Zulu',
|
||||
];
|
||||
|
||||
// TODO: move it to utils
|
||||
|
||||
export const getTzOffsetString = (date: dayjs.Dayjs) => {
|
||||
const userOffset = date.utcOffset();
|
||||
const userOffsetHours = userOffset / 60;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ describe('UserStore.unlinkBackend', () => {
|
|||
test('it makes the proper API call and returns the response', async () => {
|
||||
makeRequest.mockResolvedValueOnce('hello');
|
||||
|
||||
userStore.loadCurrentUser = jest.fn();
|
||||
Object.defineProperty(userStore, 'loadCurrentUser', { value: jest.fn() });
|
||||
|
||||
await userStore.unlinkBackend(userPk, backend);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export class UserStore extends BaseStore {
|
|||
return this.items[this.currentUserPk as User['pk']];
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async loadCurrentUser() {
|
||||
const response = await makeRequest('/user/', {});
|
||||
const timezone = await this.refreshTimezone(response.pk);
|
||||
|
|
@ -74,7 +74,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async refreshTimezone(id: User['pk']) {
|
||||
const { timezone: grafanaPreferencesTimezone } = config.bootData.user;
|
||||
const timezone = grafanaPreferencesTimezone === 'browser' ? dayjs.tz.guess() : grafanaPreferencesTimezone;
|
||||
|
|
@ -87,7 +87,7 @@ export class UserStore extends BaseStore {
|
|||
return timezone;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async loadUser(userPk: User['pk'], skipErrorHandling = false): Promise<User> {
|
||||
const user = await this.getById(userPk, skipErrorHandling);
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ export class UserStore extends BaseStore {
|
|||
return user;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItem(userPk: User['pk']) {
|
||||
if (this.itemsCurrentlyUpdating[userPk]) {
|
||||
return;
|
||||
|
|
@ -132,7 +132,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(f: any = { searchTerm: '' }, page = 1, invalidateFn?: () => boolean): Promise<any> {
|
||||
const response = await this.search(f, page);
|
||||
|
||||
|
|
@ -167,19 +167,19 @@ export class UserStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
getSearchResult() {
|
||||
getSearchResult = () => {
|
||||
return {
|
||||
page_size: this.searchResult.page_size,
|
||||
count: this.searchResult.count,
|
||||
results: this.searchResult.results?.map((userPk: User['pk']) => this.items?.[userPk]),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
sendTelegramConfirmationCode = async (userPk: User['pk']) => {
|
||||
return await makeRequest(`/users/${userPk}/get_telegram_verification_code/`, {});
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
unlinkSlack = async (userPk: User['pk']) => {
|
||||
await makeRequest(`/users/${userPk}/unlink_slack/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -195,7 +195,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
unlinkTelegram = async (userPk: User['pk']) => {
|
||||
await makeRequest(`/users/${userPk}/unlink_telegram/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -216,7 +216,7 @@ export class UserStore extends BaseStore {
|
|||
method: 'GET',
|
||||
});
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
unlinkBackend = async (userPk: User['pk'], backend: string) => {
|
||||
await makeRequest(`/users/${userPk}/unlink_backend/?backend=${backend}`, {
|
||||
method: 'POST',
|
||||
|
|
@ -225,7 +225,7 @@ export class UserStore extends BaseStore {
|
|||
this.loadCurrentUser();
|
||||
};
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async createUser(data: any) {
|
||||
const user = await this.create(data);
|
||||
|
||||
|
|
@ -239,7 +239,7 @@ export class UserStore extends BaseStore {
|
|||
return user;
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateUser(data: Partial<User>) {
|
||||
const user = await makeRequest(`/users/${data.pk}/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -261,7 +261,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateCurrentUser(data: Partial<User>) {
|
||||
const user = await makeRequest(`/user/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -279,7 +279,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async fetchVerificationCode(userPk: User['pk'], recaptchaToken: string) {
|
||||
await makeRequest(`/users/${userPk}/get_verification_code/`, {
|
||||
method: 'GET',
|
||||
|
|
@ -287,7 +287,7 @@ export class UserStore extends BaseStore {
|
|||
}).catch(throttlingError);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async fetchVerificationCall(userPk: User['pk'], recaptchaToken: string) {
|
||||
await makeRequest(`/users/${userPk}/get_verification_call/`, {
|
||||
method: 'GET',
|
||||
|
|
@ -295,21 +295,21 @@ export class UserStore extends BaseStore {
|
|||
}).catch(throttlingError);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async verifyPhone(userPk: User['pk'], token: string) {
|
||||
return await makeRequest(`/users/${userPk}/verify_number/?token=${token}`, {
|
||||
method: 'PUT',
|
||||
}).catch(throttlingError);
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async forgetPhone(userPk: User['pk']) {
|
||||
return await makeRequest(`/users/${userPk}/forget_number/`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateNotificationPolicies(id: User['pk']) {
|
||||
const importantEPs = await makeRequest('/notification_policies/', {
|
||||
params: { user: id, important: true },
|
||||
|
|
@ -327,7 +327,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async moveNotificationPolicyToPosition(userPk: User['pk'], oldIndex: number, newIndex: number, offset: number) {
|
||||
const notificationPolicy = this.notificationPolicies[userPk][oldIndex + offset];
|
||||
|
||||
|
|
@ -342,7 +342,7 @@ export class UserStore extends BaseStore {
|
|||
this.updateItem(userPk); // to update notification_chain_verbal
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async addNotificationPolicy(userPk: User['pk'], important: NotificationPolicyType['important']) {
|
||||
await makeRequest(`/notification_policies/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -354,7 +354,7 @@ export class UserStore extends BaseStore {
|
|||
this.updateItem(userPk); // to update notification_chain_verbal
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateNotificationPolicy(userPk: User['pk'], id: NotificationPolicyType['id'], value: NotificationPolicyType) {
|
||||
this.notificationPolicies = {
|
||||
...this.notificationPolicies,
|
||||
|
|
@ -380,7 +380,7 @@ export class UserStore extends BaseStore {
|
|||
this.updateItem(userPk); // to update notification_chain_verbal
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async deleteNotificationPolicy(userPk: User['pk'], id: NotificationPolicyType['id']) {
|
||||
await makeRequest(`/notification_policies/${id}`, { method: 'DELETE' }).catch(this.onApiError);
|
||||
|
||||
|
|
@ -400,7 +400,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async sendTestPushNotification(userId: User['pk'], isCritical: boolean) {
|
||||
return await makeRequest(`/users/${userId}/send_test_push`, {
|
||||
method: 'POST',
|
||||
|
|
@ -419,7 +419,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async makeTestCall(userPk: User['pk']) {
|
||||
this.isTestCallInProgress = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class UserGroupStore extends BaseStore {
|
|||
this.path = '/user_groups/';
|
||||
}
|
||||
|
||||
@action
|
||||
@action.bound
|
||||
async updateItems(query = '') {
|
||||
const result = await makeRequest(`${this.path}`, {
|
||||
params: { search: query },
|
||||
|
|
@ -46,11 +46,11 @@ export class UserGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
getSearchResult = (query = '') => {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.searchResult[query].map((userGroupId: UserGroup['id']) => this.items?.[userGroupId]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@ import { SpanStatusCode } from '@opentelemetry/api';
|
|||
|
||||
import { FaroHelper } from 'utils/faro';
|
||||
|
||||
import { customFetch } from './http-client';
|
||||
import { getCustomFetchFn } from './http-client';
|
||||
|
||||
jest.mock('utils/faro', () => ({
|
||||
__esModule: true,
|
||||
|
|
@ -31,6 +31,7 @@ const REQUEST_CONFIG = {
|
|||
const URL = 'https://someurl.com';
|
||||
const SUCCESSFUL_RESPONSE_MOCK = { ok: true };
|
||||
const ERROR_MOCK = 'error';
|
||||
const customFetch = getCustomFetchFn({ withGlobalErrorHandler: true });
|
||||
|
||||
describe('customFetch', () => {
|
||||
beforeAll(() => {
|
||||
|
|
@ -54,8 +55,8 @@ describe('customFetch', () => {
|
|||
describe('if response is not successful', () => {
|
||||
it('should push event and error to faro', async () => {
|
||||
(FaroHelper.faro.api.getOTEL as unknown as jest.Mock).mockReturnValueOnce(undefined);
|
||||
fetchMock.mockRejectedValueOnce(ERROR_MOCK);
|
||||
await expect(customFetch(URL, REQUEST_CONFIG)).rejects.toEqual(Error(ERROR_MOCK));
|
||||
fetchMock.mockResolvedValueOnce({ ok: false, json: () => ERROR_MOCK });
|
||||
await expect(customFetch(URL, REQUEST_CONFIG)).rejects.toEqual(ERROR_MOCK);
|
||||
expect(FaroHelper.faro.api.pushEvent).toHaveBeenCalledWith('Request failed', { url: URL });
|
||||
expect(FaroHelper.faro.api.pushError).toHaveBeenCalledWith(ERROR_MOCK);
|
||||
});
|
||||
|
|
@ -113,7 +114,7 @@ describe('customFetch', () => {
|
|||
|
||||
describe('if response is not successful', () => {
|
||||
it('should reject Promise, push event to faro, set span status to error and end span', async () => {
|
||||
fetchMock.mockRejectedValueOnce(ERROR_MOCK);
|
||||
fetchMock.mockResolvedValueOnce({ ok: false, json: () => ERROR_MOCK });
|
||||
await expect(customFetch(URL, REQUEST_CONFIG)).rejects.toEqual(ERROR_MOCK);
|
||||
expect(FaroHelper.faro.api.pushEvent).toHaveBeenCalledWith('Request failed', { url: URL });
|
||||
expect(FaroHelper.faro.api.pushError).toHaveBeenCalledWith(ERROR_MOCK);
|
||||
|
|
|
|||
|
|
@ -4,75 +4,108 @@ import createClient from 'openapi-fetch';
|
|||
import qs from 'query-string';
|
||||
|
||||
import { FaroHelper } from 'utils/faro';
|
||||
import { formatBackendError, openErrorNotification } from 'utils/utils';
|
||||
|
||||
import { paths } from './autogenerated-api.types';
|
||||
|
||||
export const API_PROXY_PREFIX = 'api/plugin-proxy/grafana-oncall-app';
|
||||
export const API_PATH_PREFIX = '/api/internal/v1';
|
||||
|
||||
export const customFetch = async (url: string, requestConfig: Parameters<typeof fetch>[1] = {}): Promise<Response> => {
|
||||
const { faro } = FaroHelper;
|
||||
const otel = faro?.api?.getOTEL();
|
||||
|
||||
if (faro && otel) {
|
||||
const tracer = otel.trace.getTracer('default');
|
||||
let span = otel.trace.getActiveSpan();
|
||||
|
||||
if (!span) {
|
||||
span = tracer.startSpan('http-request');
|
||||
span.setAttribute('page_url', document.URL.split('//')[1]);
|
||||
span.setAttribute(SemanticAttributes.HTTP_URL, url);
|
||||
span.setAttribute(SemanticAttributes.HTTP_METHOD, requestConfig.method);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
otel.context.with(otel.trace.setSpan(otel.context.active(), span), async () => {
|
||||
faro.api.pushEvent('Sending request', { url });
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...requestConfig,
|
||||
headers: {
|
||||
...requestConfig.headers,
|
||||
/**
|
||||
* In short, this header will tell the Grafana plugin proxy, a Go service which use Go's HTTP Transport,
|
||||
* to retry POST requests (and other non-idempotent requests). This doesn't necessarily make these requests
|
||||
* idempotent, but it will make them retry-able from Go's (read: net/http) perspective.
|
||||
*
|
||||
* https://stackoverflow.com/questions/42847294/how-to-catch-http-server-closed-idle-connection-error/62292758#62292758
|
||||
* https://raintank-corp.slack.com/archives/C01C4K8DETW/p1692280544382739?thread_ts=1692279329.797149&cid=C01C4K8DETW
|
||||
*/ 'X-Idempotency-Key': `${Date.now()}-${Math.random()}`,
|
||||
},
|
||||
});
|
||||
faro.api.pushEvent('Request completed', { url });
|
||||
span.end();
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
faro.api.pushEvent('Request failed', { url });
|
||||
faro.api.pushError(error);
|
||||
span.setStatus({ code: SpanStatusCode.ERROR });
|
||||
span.end();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const response = await fetch(url, requestConfig);
|
||||
faro?.api.pushEvent('Request completed', { url });
|
||||
return response;
|
||||
} catch (error) {
|
||||
faro?.api.pushEvent('Request failed', { url });
|
||||
faro?.api.pushError(error);
|
||||
throw new Error(error);
|
||||
const showApiError = (errorResponse: Response) => {
|
||||
if (errorResponse.status >= 400 && errorResponse.status < 500) {
|
||||
const text = formatBackendError(errorResponse.statusText);
|
||||
if (text) {
|
||||
openErrorNotification(text);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onCallApi = createClient<paths>({
|
||||
export const getCustomFetchFn =
|
||||
({ withGlobalErrorHandler }: { withGlobalErrorHandler: boolean }) =>
|
||||
async (url: string, reqConfig: Parameters<typeof fetch>[1] = {}): Promise<Response> => {
|
||||
const { faro } = FaroHelper;
|
||||
const otel = faro?.api?.getOTEL();
|
||||
const requestConfig = {
|
||||
...reqConfig,
|
||||
headers: {
|
||||
...reqConfig.headers,
|
||||
'Content-Type': 'application/json',
|
||||
/**
|
||||
* In short, this header will tell the Grafana plugin proxy, a Go service which use Go's HTTP Transport,
|
||||
* to retry POST requests (and other non-idempotent requests). This doesn't necessarily make these requests
|
||||
* idempotent, but it will make them retry-able from Go's (read: net/http) perspective.
|
||||
*
|
||||
* https://stackoverflow.com/questions/42847294/how-to-catch-http-server-closed-idle-connection-error/62292758#62292758
|
||||
* https://raintank-corp.slack.com/archives/C01C4K8DETW/p1692280544382739?thread_ts=1692279329.797149&cid=C01C4K8DETW
|
||||
*/ 'X-Idempotency-Key': `${Date.now()}-${Math.random()}`,
|
||||
},
|
||||
};
|
||||
|
||||
if (faro && otel) {
|
||||
const tracer = otel.trace.getTracer('default');
|
||||
let span = otel.trace.getActiveSpan();
|
||||
|
||||
if (!span) {
|
||||
span = tracer.startSpan('http-request');
|
||||
span.setAttribute('page_url', document.URL.split('//')[1]);
|
||||
span.setAttribute(SemanticAttributes.HTTP_URL, url);
|
||||
span.setAttribute(SemanticAttributes.HTTP_METHOD, requestConfig.method);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
otel.context.with(otel.trace.setSpan(otel.context.active(), span), async () => {
|
||||
faro.api.pushEvent('Sending request', { url });
|
||||
|
||||
const res = await fetch(url, requestConfig);
|
||||
|
||||
if (res.ok) {
|
||||
faro.api.pushEvent('Request completed', { url });
|
||||
span.end();
|
||||
resolve(res);
|
||||
} else {
|
||||
const errorData = await res.json();
|
||||
faro.api.pushEvent('Request failed', { url });
|
||||
faro.api.pushError(errorData);
|
||||
span.setStatus({ code: SpanStatusCode.ERROR });
|
||||
span.end();
|
||||
if (withGlobalErrorHandler) {
|
||||
showApiError(res);
|
||||
}
|
||||
reject(errorData);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(url, requestConfig);
|
||||
if (res.ok) {
|
||||
faro?.api.pushEvent('Request completed', { url });
|
||||
return res;
|
||||
} else {
|
||||
const errorData = await res.json();
|
||||
faro?.api.pushEvent('Request failed', { url });
|
||||
faro?.api.pushError(errorData);
|
||||
if (withGlobalErrorHandler) {
|
||||
showApiError(res);
|
||||
}
|
||||
throw errorData;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clientConfig = {
|
||||
baseUrl: `${API_PROXY_PREFIX}${API_PATH_PREFIX}`,
|
||||
querySerializer: (params: unknown) => qs.stringify(params, { arrayFormat: 'none' }),
|
||||
fetch: customFetch,
|
||||
};
|
||||
|
||||
// We might want to switch to middleware instead of 2 clients once this is published: https://github.com/drwpow/openapi-typescript/pull/1521
|
||||
const onCallApiWithGlobalErrorHandling = createClient<paths>({
|
||||
...clientConfig,
|
||||
fetch: getCustomFetchFn({ withGlobalErrorHandler: true }),
|
||||
});
|
||||
const onCallApiSkipErrorHandling = createClient<paths>({
|
||||
...clientConfig,
|
||||
fetch: getCustomFetchFn({ withGlobalErrorHandler: false }),
|
||||
});
|
||||
|
||||
export default onCallApi;
|
||||
export const onCallApi = ({ skipErrorHandling = false }: { skipErrorHandling?: boolean } = {}) =>
|
||||
skipErrorHandling ? onCallApiSkipErrorHandling : onCallApiWithGlobalErrorHandling;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import { prepareForUpdate } from 'containers/AddResponders/AddResponders.helpers
|
|||
import { UserResponder } from 'containers/AddResponders/AddResponders.types';
|
||||
import { AttachIncidentForm } from 'containers/AttachIncidentForm/AttachIncidentForm';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { Alert, AlertAction, TimeLineItem, TimeLineRealm, GroupedAlert } from 'models/alertgroup/alertgroup.types';
|
||||
import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
|
|
@ -270,17 +271,14 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
params: { id },
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const { alerts } = store.alertGroupStore;
|
||||
|
||||
const incident = alerts.get(id);
|
||||
|
||||
const integration = store.alertReceiveChannelStore.getIntegration(incident.alert_receive_channel);
|
||||
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(
|
||||
store.alertReceiveChannelStore,
|
||||
incident.alert_receive_channel
|
||||
);
|
||||
const showLinkTo = !incident.dependent_alert_groups.length && !incident.root_alert_group && !incident.resolved;
|
||||
|
||||
const integrationNameWithEmojies = <Emoji text={incident.alert_receive_channel.verbal_name} />;
|
||||
|
||||
const sourceLink = incident?.render_for_web?.source_link;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { IncidentsFiltersType } from 'containers/IncidentsFilters/IncidentFilter
|
|||
import { RemoteFilters } from 'containers/RemoteFilters/RemoteFilters';
|
||||
import { TeamName } from 'containers/TeamName/TeamName';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import {
|
||||
Alert,
|
||||
Alert as AlertType,
|
||||
|
|
@ -629,7 +630,10 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
const integration = alertReceiveChannelStore.getIntegration(record.alert_receive_channel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(
|
||||
alertReceiveChannelStore,
|
||||
record.alert_receive_channel
|
||||
);
|
||||
|
||||
return (
|
||||
<TextEllipsisTooltip
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { IconName } from '@grafana/ui';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { AlertReceiveChannel, MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { RootStore } from 'state/rootStore';
|
||||
|
||||
import { MAX_CHARACTERS_COUNT, TEXTAREA_ROWS_COUNT } from './IntegrationCommon.config';
|
||||
|
||||
export const IntegrationHelper = {
|
||||
isSpecificIntegration: (alertReceiveChannel: AlertReceiveChannel | string, name: string) => {
|
||||
isSpecificIntegration: (alertReceiveChannel: ApiSchemas['AlertReceiveChannel'] | string, name: string) => {
|
||||
if (!alertReceiveChannel) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -46,7 +47,7 @@ export const IntegrationHelper = {
|
|||
return slice.length === line.length ? slice : `${slice} ...`;
|
||||
},
|
||||
|
||||
getMaintenanceText(maintenanceUntill: number, mode: number = undefined) {
|
||||
getMaintenanceText(maintenanceUntill: number, mode?: MaintenanceMode) {
|
||||
const date = dayjs(new Date(maintenanceUntill * 1000));
|
||||
const now = dayjs();
|
||||
const hourDiff = date.diff(now, 'hours');
|
||||
|
|
@ -124,4 +125,5 @@ export const IntegrationHelper = {
|
|||
},
|
||||
};
|
||||
|
||||
export const getIsBidirectionalIntegration = ({ integration }: AlertReceiveChannel) => integration === 'servicenow';
|
||||
export const getIsBidirectionalIntegration = ({ integration }: ApiSchemas['AlertReceiveChannel']) =>
|
||||
integration === ('servicenow' as ApiSchemas['AlertReceiveChannel']['integration']); // TODO: add service now in backend schema as valid value and remove casting
|
||||
|
|
|
|||
|
|
@ -54,12 +54,11 @@ import { TeamName } from 'containers/TeamName/TeamName';
|
|||
import { UserDisplayWithAvatar } from 'containers/UserDisplay/UserDisplayWithAvatar';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { HeartIcon, HeartRedIcon } from 'icons/Icons';
|
||||
import {
|
||||
AlertReceiveChannel,
|
||||
AlertReceiveChannelCounters,
|
||||
} from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { AlertReceiveChannelCounters } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { IntegrationHelper, getIsBidirectionalIntegration } from 'pages/integration/Integration.helper';
|
||||
import styles from 'pages/integration/Integration.module.scss';
|
||||
import { AppFeature } from 'state/features';
|
||||
|
|
@ -154,7 +153,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
);
|
||||
}
|
||||
|
||||
const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel);
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id];
|
||||
const isLegacyIntegration = integration && (integration?.value as string).toLowerCase().startsWith('legacy_');
|
||||
const contactPoints = alertReceiveChannelStore.connectedContactPoints?.[alertReceiveChannel.id];
|
||||
|
|
@ -331,7 +330,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
}
|
||||
}
|
||||
|
||||
renderAlertmanagerV2MigrationHeaderMaybe(alertReceiveChannel: AlertReceiveChannel) {
|
||||
renderAlertmanagerV2MigrationHeaderMaybe(alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) {
|
||||
if (!alertReceiveChannel.alertmanager_v2_migrated_at) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -390,7 +389,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
);
|
||||
}
|
||||
|
||||
renderDescriptionMaybe(alertReceiveChannel: AlertReceiveChannel) {
|
||||
renderDescriptionMaybe(alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) {
|
||||
if (!alertReceiveChannel.description_short) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -402,7 +401,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
);
|
||||
}
|
||||
|
||||
renderContactPointsWarningMaybe(alertReceiveChannel: AlertReceiveChannel) {
|
||||
renderContactPointsWarningMaybe(alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) {
|
||||
if (IntegrationHelper.isSpecificIntegration(alertReceiveChannel, 'grafana_alerting')) {
|
||||
return (
|
||||
<div className={cx('u-padding-top-md')}>
|
||||
|
|
@ -583,14 +582,13 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
isAddingRoute: true,
|
||||
},
|
||||
() => {
|
||||
alertReceiveChannelStore
|
||||
.createChannelFilter({
|
||||
alert_receive_channel: id,
|
||||
filtering_term: NEW_ROUTE_DEFAULT,
|
||||
filtering_term_type: 1, // non-regex
|
||||
})
|
||||
AlertReceiveChannelHelper.createChannelFilter({
|
||||
alert_receive_channel: id,
|
||||
filtering_term: NEW_ROUTE_DEFAULT,
|
||||
filtering_term_type: 1, // non-regex
|
||||
})
|
||||
.then(async (channelFilter: ChannelFilter) => {
|
||||
await alertReceiveChannelStore.updateChannelFilters(id);
|
||||
await alertReceiveChannelStore.fetchChannelFilters(id);
|
||||
|
||||
this.setState(
|
||||
(prevState) => ({
|
||||
|
|
@ -693,7 +691,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
filtering_term_type: filteringTermType,
|
||||
})
|
||||
.then((channelFilter: ChannelFilter) => {
|
||||
alertReceiveChannelStore.updateChannelFilters(id, true).then(() => {
|
||||
alertReceiveChannelStore.fetchChannelFilters(id, true).then(() => {
|
||||
escalationPolicyStore.updateEscalationPolicies(channelFilter.escalation_chain);
|
||||
});
|
||||
this.setState({
|
||||
|
|
@ -753,13 +751,10 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
}
|
||||
};
|
||||
|
||||
onRemovalFn = (id: AlertReceiveChannel['id']) => {
|
||||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
history,
|
||||
} = this.props;
|
||||
|
||||
alertReceiveChannelStore.deleteAlertReceiveChannel(id).then(() => history.push(`${PLUGIN_ROOT}/integrations/`));
|
||||
onRemovalFn = (id: ApiSchemas['AlertReceiveChannel']['id']) => {
|
||||
AlertReceiveChannelHelper.deleteAlertReceiveChannel(id).then(() =>
|
||||
this.props.history.push(`${PLUGIN_ROOT}/integrations/`)
|
||||
);
|
||||
};
|
||||
|
||||
async loadData() {
|
||||
|
|
@ -775,18 +770,18 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
const promises = [];
|
||||
|
||||
if (!alertReceiveChannelStore.items[id]) {
|
||||
promises.push(alertReceiveChannelStore.loadItem(id).then(() => this.loadExtraData(id)));
|
||||
promises.push(alertReceiveChannelStore.fetchItemById(id).then(() => this.loadExtraData(id)));
|
||||
} else {
|
||||
promises.push(this.loadExtraData(id));
|
||||
}
|
||||
|
||||
if (!alertReceiveChannelStore.channelFilterIds[id]) {
|
||||
promises.push(alertReceiveChannelStore.updateChannelFilters(id));
|
||||
promises.push(alertReceiveChannelStore.fetchChannelFilters(id));
|
||||
}
|
||||
|
||||
promises.push(alertReceiveChannelStore.updateTemplates(id));
|
||||
promises.push(alertReceiveChannelStore.fetchTemplates(id));
|
||||
promises.push(IntegrationHelper.fetchChatOps(store));
|
||||
promises.push(alertReceiveChannelStore.updateCountersForIntegration(id));
|
||||
promises.push(alertReceiveChannelStore.fetchCountersForIntegration(id));
|
||||
|
||||
await Promise.all(promises)
|
||||
.catch(() => {
|
||||
|
|
@ -798,12 +793,12 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
.finally(() => this.setState({ isLoading: false }));
|
||||
}
|
||||
|
||||
async loadExtraData(id: AlertReceiveChannel['id']) {
|
||||
async loadExtraData(id: ApiSchemas['AlertReceiveChannel']['id']) {
|
||||
const { alertReceiveChannelStore } = this.props.store;
|
||||
|
||||
if (IntegrationHelper.isSpecificIntegration(alertReceiveChannelStore.items[id], 'grafana_alerting')) {
|
||||
// this will be delayed and not awaitable so that we don't delay the whole page load
|
||||
return await alertReceiveChannelStore.updateConnectedContactPoints(id);
|
||||
return await alertReceiveChannelStore.fetchConnectedContactPoints(id);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
|
|
@ -812,7 +807,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
|
||||
interface IntegrationActionsProps {
|
||||
isLegacyIntegration: boolean;
|
||||
alertReceiveChannel: AlertReceiveChannel;
|
||||
alertReceiveChannel: ApiSchemas['AlertReceiveChannel'];
|
||||
changeIsTemplateSettingsOpen: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -843,7 +838,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
const [isDemoModalOpen, setIsDemoModalOpen] = useState(false);
|
||||
const [maintenanceData, setMaintenanceData] = useState<{
|
||||
disabled: boolean;
|
||||
alert_receive_channel_id: AlertReceiveChannel['id'];
|
||||
alert_receive_channel_id: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
}>(undefined);
|
||||
|
||||
const { id } = alertReceiveChannel;
|
||||
|
|
@ -876,9 +871,11 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
<IntegrationForm
|
||||
isTableView={false}
|
||||
onHide={() => setIsIntegrationSettingsOpen(false)}
|
||||
onSubmit={() => alertReceiveChannelStore.updateItem(alertReceiveChannel['id'])}
|
||||
onSubmit={async () => {
|
||||
await alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id);
|
||||
}}
|
||||
id={alertReceiveChannel['id']}
|
||||
navigateToAlertGroupLabels={(_id: AlertReceiveChannel['id']) => {
|
||||
navigateToAlertGroupLabels={(_id: ApiSchemas['AlertReceiveChannel']['id']) => {
|
||||
setIsIntegrationSettingsOpen(false);
|
||||
setLabelsFormOpen(true);
|
||||
}}
|
||||
|
|
@ -890,7 +887,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
onHide={() => {
|
||||
setLabelsFormOpen(false);
|
||||
}}
|
||||
onSubmit={() => alertReceiveChannelStore.updateItem(alertReceiveChannel['id'])}
|
||||
onSubmit={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)}
|
||||
id={alertReceiveChannel['id']}
|
||||
onOpenIntegrationSettings={() => {
|
||||
setIsIntegrationSettingsOpen(true);
|
||||
|
|
@ -908,7 +905,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
{maintenanceData && (
|
||||
<MaintenanceForm
|
||||
initialData={maintenanceData}
|
||||
onUpdate={() => alertReceiveChannelStore.updateItem(alertReceiveChannel.id)}
|
||||
onUpdate={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)}
|
||||
onHide={() => setMaintenanceData(undefined)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1104,16 +1101,15 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
}
|
||||
|
||||
function onIntegrationMigrate() {
|
||||
alertReceiveChannelStore
|
||||
.migrateChannel(alertReceiveChannel.id)
|
||||
AlertReceiveChannelHelper.migrateChannel(alertReceiveChannel.id)
|
||||
.then(() => {
|
||||
setConfirmModal(undefined);
|
||||
openNotification('Integration has been successfully migrated.');
|
||||
})
|
||||
.then(() =>
|
||||
Promise.all([
|
||||
alertReceiveChannelStore.updateItem(alertReceiveChannel.id),
|
||||
alertReceiveChannelStore.updateTemplates(alertReceiveChannel.id),
|
||||
alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id),
|
||||
alertReceiveChannelStore.fetchTemplates(alertReceiveChannel.id),
|
||||
])
|
||||
)
|
||||
.catch(() => openErrorNotification('An error has occurred. Please try again.'));
|
||||
|
|
@ -1124,8 +1120,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
}
|
||||
|
||||
function deleteIntegration() {
|
||||
alertReceiveChannelStore
|
||||
.deleteAlertReceiveChannel(alertReceiveChannel.id)
|
||||
AlertReceiveChannelHelper.deleteAlertReceiveChannel(alertReceiveChannel.id)
|
||||
.then(() => history.push(`${PLUGIN_ROOT}/integrations`))
|
||||
.then(() => openNotification('Integration has been succesfully deleted.'))
|
||||
.catch(() => openErrorNotification('An error has occurred. Please try again.'));
|
||||
|
|
@ -1146,16 +1141,16 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
async function onStopMaintenance() {
|
||||
setConfirmModal(undefined);
|
||||
|
||||
await alertReceiveChannelStore.stopMaintenanceMode(id);
|
||||
await AlertReceiveChannelHelper.stopMaintenanceMode(id);
|
||||
|
||||
openNotification('Maintenance has been stopped');
|
||||
await alertReceiveChannelStore.updateItem(id);
|
||||
await alertReceiveChannelStore.fetchItemById(id);
|
||||
}
|
||||
};
|
||||
|
||||
interface IntegrationHeaderProps {
|
||||
alertReceiveChannelCounter: AlertReceiveChannelCounters;
|
||||
alertReceiveChannel: AlertReceiveChannel;
|
||||
alertReceiveChannel: ApiSchemas['AlertReceiveChannel'];
|
||||
integration: SelectOption;
|
||||
renderLabels: boolean;
|
||||
}
|
||||
|
|
@ -1269,7 +1264,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
function renderHeartbeat(alertReceiveChannel: AlertReceiveChannel) {
|
||||
function renderHeartbeat(alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) {
|
||||
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
||||
const heartbeat = heartbeatStore.items[heartbeatId];
|
||||
|
||||
|
|
|
|||
|
|
@ -41,11 +41,10 @@ import { TeamName } from 'containers/TeamName/TeamName';
|
|||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { HeartIcon, HeartRedIcon } from 'icons/Icons';
|
||||
import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel';
|
||||
import {
|
||||
AlertReceiveChannel,
|
||||
MaintenanceMode,
|
||||
SupportedIntegrationFilters,
|
||||
} from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { operations } from 'network/oncall-api/autogenerated-api.types';
|
||||
import { IntegrationHelper } from 'pages/integration/Integration.helper';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
|
|
@ -79,9 +78,9 @@ const cx = cn.bind(styles);
|
|||
const FILTERS_DEBOUNCE_MS = 500;
|
||||
|
||||
interface IntegrationsState extends PageBaseState {
|
||||
integrationsFilters: SupportedIntegrationFilters;
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'] | 'new';
|
||||
alertReceiveChannelIdToShowLabels?: AlertReceiveChannel['id'];
|
||||
integrationsFilters: operations['alert_receive_channels_list']['parameters']['query'];
|
||||
alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id'] | 'new';
|
||||
alertReceiveChannelIdToShowLabels?: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
confirmationModal: {
|
||||
isOpen: boolean;
|
||||
title: any;
|
||||
|
|
@ -103,7 +102,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
integrationsFilters: { searchTerm: '', integration_ne: ['direct_paging'] },
|
||||
integrationsFilters: { integration_ne: ['direct_paging'] },
|
||||
errorData: initErrorDataState(),
|
||||
confirmationModal: undefined,
|
||||
activeTab: props.query[TAB_QUERY_PARAM_KEY] || TabType.MonitoringSystems,
|
||||
|
|
@ -140,12 +139,12 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
return;
|
||||
}
|
||||
|
||||
let alertReceiveChannel: AlertReceiveChannel | void = undefined;
|
||||
let alertReceiveChannel: ApiSchemas['AlertReceiveChannel'] | void = undefined;
|
||||
const isNewAlertReceiveChannel = id === 'new';
|
||||
|
||||
if (!isNewAlertReceiveChannel) {
|
||||
alertReceiveChannel = await store.alertReceiveChannelStore
|
||||
.loadItem(id, true)
|
||||
.fetchItemById(id, true)
|
||||
.catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } }));
|
||||
}
|
||||
|
||||
|
|
@ -157,25 +156,25 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
getFiltersBasedOnCurrentTab = () => ({
|
||||
...this.state.integrationsFilters,
|
||||
...(this.state.activeTab === TabType.DirectPaging
|
||||
? { integration: ['direct_paging'] }
|
||||
? { integration: ['direct_paging' as const] }
|
||||
: {
|
||||
integration_ne: ['direct_paging'],
|
||||
integration_ne: ['direct_paging' as const],
|
||||
integration: this.state.integrationsFilters.integration?.filter(
|
||||
(integration) => integration !== 'direct_paging'
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
update = () => {
|
||||
update = async () => {
|
||||
const { store } = this.props;
|
||||
const page = store.filtersStore.currentTablePageNum[PAGE.Integrations];
|
||||
|
||||
LocationHelper.update({ p: page }, 'partial');
|
||||
|
||||
return store.alertReceiveChannelStore.updatePaginatedItems({
|
||||
await store.alertReceiveChannelStore.fetchPaginatedItems({
|
||||
filters: this.getFiltersBasedOnCurrentTab(),
|
||||
page,
|
||||
updateCounters: false,
|
||||
shouldFetchCounters: false,
|
||||
invalidateFn: () => this.invalidateRequestFn(page),
|
||||
});
|
||||
};
|
||||
|
|
@ -202,8 +201,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
integrationsFilters,
|
||||
} = this.state;
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
const { count, results, page_size } = alertReceiveChannelStore.getPaginatedSearchResult();
|
||||
const { count, results, page_size } = AlertReceiveChannelHelper.getPaginatedSearchResult(alertReceiveChannelStore);
|
||||
const isDirectPagingSelectedOnMonitoringSystemsTab =
|
||||
activeTab === TabType.MonitoringSystems && integrationsFilters.integration?.includes('direct_paging');
|
||||
|
||||
|
|
@ -293,7 +291,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
}}
|
||||
onSubmit={this.update}
|
||||
id={alertReceiveChannelId}
|
||||
navigateToAlertGroupLabels={(id: AlertReceiveChannel['id']) => {
|
||||
navigateToAlertGroupLabels={(id: ApiSchemas['AlertReceiveChannel']['id']) => {
|
||||
this.setState({ alertReceiveChannelId: undefined, alertReceiveChannelIdToShowLabels: id });
|
||||
}}
|
||||
/>
|
||||
|
|
@ -306,7 +304,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
}}
|
||||
onSubmit={this.update}
|
||||
id={alertReceiveChannelIdToShowLabels}
|
||||
onOpenIntegrationSettings={(id: AlertReceiveChannel['id']) => {
|
||||
onOpenIntegrationSettings={(id: ApiSchemas['AlertReceiveChannel']['id']) => {
|
||||
this.setState({ alertReceiveChannelId: id });
|
||||
}}
|
||||
/>
|
||||
|
|
@ -333,7 +331,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
);
|
||||
}
|
||||
|
||||
renderName = (item: AlertReceiveChannel) => {
|
||||
renderName = (item: ApiSchemas['AlertReceiveChannel']) => {
|
||||
const { query } = this.props;
|
||||
|
||||
return (
|
||||
|
|
@ -353,9 +351,9 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
);
|
||||
};
|
||||
|
||||
renderDatasource(item: AlertReceiveChannel, alertReceiveChannelStore: AlertReceiveChannelStore) {
|
||||
renderDatasource(item: ApiSchemas['AlertReceiveChannel'], alertReceiveChannelStore: AlertReceiveChannelStore) {
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
|
||||
const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel);
|
||||
const isLegacyIntegration = (integration?.value as string)?.toLowerCase().startsWith('legacy_');
|
||||
|
||||
if (isLegacyIntegration) {
|
||||
|
|
@ -379,7 +377,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
);
|
||||
}
|
||||
|
||||
renderIntegrationStatus(item: AlertReceiveChannel, alertReceiveChannelStore: AlertReceiveChannelStore) {
|
||||
renderIntegrationStatus(item: ApiSchemas['AlertReceiveChannel'], alertReceiveChannelStore: AlertReceiveChannelStore) {
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[item.id];
|
||||
let routesCounter = item.routes_count;
|
||||
let connectedEscalationsChainsCount = item.connected_escalations_chains_count;
|
||||
|
|
@ -426,7 +424,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
);
|
||||
}
|
||||
|
||||
renderHeartbeat(item: AlertReceiveChannel) {
|
||||
renderHeartbeat(item: ApiSchemas['AlertReceiveChannel']) {
|
||||
const { store } = this.props;
|
||||
const { alertReceiveChannelStore, heartbeatStore } = store;
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
|
||||
|
|
@ -453,7 +451,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
);
|
||||
}
|
||||
|
||||
renderMaintenance(item: AlertReceiveChannel) {
|
||||
renderMaintenance(item: ApiSchemas['AlertReceiveChannel']) {
|
||||
const maintenanceMode = item.maintenance_mode;
|
||||
|
||||
if (maintenanceMode === MaintenanceMode.Debug || maintenanceMode === MaintenanceMode.Maintenance) {
|
||||
|
|
@ -474,7 +472,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
return null;
|
||||
}
|
||||
|
||||
renderTeam(item: AlertReceiveChannel, teams: any) {
|
||||
renderTeam(item: ApiSchemas['AlertReceiveChannel'], teams: any) {
|
||||
return (
|
||||
<TextEllipsisTooltip placement="top" content={teams[item.team]?.name}>
|
||||
<TeamName className={TEXT_ELLIPSIS_CLASS} team={teams[item.team]} />
|
||||
|
|
@ -482,7 +480,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
);
|
||||
}
|
||||
|
||||
renderButtons = (item: AlertReceiveChannel) => {
|
||||
renderButtons = (item: ApiSchemas['AlertReceiveChannel']) => {
|
||||
const { store } = this.props;
|
||||
|
||||
return (
|
||||
|
|
@ -573,13 +571,14 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
width: '15%',
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
render: (item: AlertReceiveChannel) => this.renderIntegrationStatus(item, alertReceiveChannelStore),
|
||||
render: (item: ApiSchemas['AlertReceiveChannel']) =>
|
||||
this.renderIntegrationStatus(item, alertReceiveChannelStore),
|
||||
},
|
||||
{
|
||||
width: '25%',
|
||||
title: 'Type',
|
||||
key: 'datasource',
|
||||
render: (item: AlertReceiveChannel) => this.renderDatasource(item, alertReceiveChannelStore),
|
||||
render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderDatasource(item, alertReceiveChannelStore),
|
||||
},
|
||||
...(isMonitoringSystemsTab
|
||||
? [
|
||||
|
|
@ -587,25 +586,25 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
width: '10%',
|
||||
title: 'Maintenance',
|
||||
key: 'maintenance',
|
||||
render: (item: AlertReceiveChannel) => this.renderMaintenance(item),
|
||||
render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderMaintenance(item),
|
||||
},
|
||||
{
|
||||
width: '5%',
|
||||
title: 'Heartbeat',
|
||||
key: 'heartbeat',
|
||||
render: (item: AlertReceiveChannel) => this.renderHeartbeat(item),
|
||||
render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderHeartbeat(item),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
width: isMonitoringSystemsTab ? '15%' : '30%',
|
||||
title: 'Team',
|
||||
render: (item: AlertReceiveChannel) => this.renderTeam(item, grafanaTeamStore.items),
|
||||
render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderTeam(item, grafanaTeamStore.items),
|
||||
},
|
||||
{
|
||||
width: '50px',
|
||||
key: 'buttons',
|
||||
render: (item: AlertReceiveChannel) => this.renderButtons(item),
|
||||
render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderButtons(item),
|
||||
className: cx('buttons'),
|
||||
},
|
||||
];
|
||||
|
|
@ -614,7 +613,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
columns.splice(-2, 0, {
|
||||
width: '10%',
|
||||
title: 'Labels',
|
||||
render: ({ labels }: AlertReceiveChannel) => (
|
||||
render: ({ labels }: ApiSchemas['AlertReceiveChannel']) => (
|
||||
<LabelsTooltipBadge labels={labels} onClick={(label) => applyLabelFilter(label, PAGE.Integrations)} />
|
||||
),
|
||||
});
|
||||
|
|
@ -636,20 +635,16 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
this.update();
|
||||
};
|
||||
|
||||
onIntegrationEditClick = (id: AlertReceiveChannel['id']) => {
|
||||
onIntegrationEditClick = (id: ApiSchemas['AlertReceiveChannel']['id']) => {
|
||||
this.setState({ alertReceiveChannelId: id });
|
||||
};
|
||||
|
||||
onLabelsEditClick = (id: AlertReceiveChannel['id']) => {
|
||||
onLabelsEditClick = (id: ApiSchemas['AlertReceiveChannel']['id']) => {
|
||||
this.setState({ alertReceiveChannelIdToShowLabels: id });
|
||||
};
|
||||
|
||||
handleDeleteAlertReceiveChannel = (alertReceiveChannelId: AlertReceiveChannel['id']) => {
|
||||
const { store } = this.props;
|
||||
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
alertReceiveChannelStore.deleteAlertReceiveChannel(alertReceiveChannelId).then(this.applyFilters);
|
||||
handleDeleteAlertReceiveChannel = (alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']) => {
|
||||
AlertReceiveChannelHelper.deleteAlertReceiveChannel(alertReceiveChannelId).then(this.applyFilters);
|
||||
this.setState({ confirmationModal: undefined });
|
||||
};
|
||||
|
||||
|
|
@ -666,10 +661,10 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
const newPage = isOnMount ? store.filtersStore.currentTablePageNum[PAGE.Integrations] : 1;
|
||||
|
||||
return alertReceiveChannelStore
|
||||
.updatePaginatedItems({
|
||||
.fetchPaginatedItems({
|
||||
filters: this.getFiltersBasedOnCurrentTab(),
|
||||
page: newPage,
|
||||
updateCounters: false,
|
||||
shouldFetchCounters: false,
|
||||
invalidateFn: () => this.invalidateRequestFn(newPage),
|
||||
})
|
||||
.then(() => {
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ class _SlackSettings extends Component<SlackProps, SlackState> {
|
|||
const {
|
||||
organizationStore: { currentOrganization },
|
||||
slackStore,
|
||||
slackChannelStore,
|
||||
} = store;
|
||||
|
||||
return (
|
||||
|
|
@ -119,9 +120,12 @@ class _SlackSettings extends Component<SlackProps, SlackState> {
|
|||
tooltip="The selected channel will be used as a fallback in the event that a schedule or integration does not have a configured channel"
|
||||
>
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsUpdateSettings}>
|
||||
<GSelect
|
||||
<GSelect<SlackChannel>
|
||||
showSearch
|
||||
modelName="slackChannelStore"
|
||||
items={slackChannelStore.items}
|
||||
fetchItemsFn={slackChannelStore.updateItems}
|
||||
fetchItemFn={slackChannelStore.updateItem}
|
||||
getSearchResult={slackChannelStore.getSearchResult}
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Slack Channel"
|
||||
|
|
@ -201,17 +205,22 @@ class _SlackSettings extends Component<SlackProps, SlackState> {
|
|||
};
|
||||
|
||||
renderSlackChannels = () => {
|
||||
const { store } = this.props;
|
||||
const {
|
||||
store: { organizationStore, slackChannelStore },
|
||||
} = this.props;
|
||||
return (
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsUpdateSettings}>
|
||||
<GSelect
|
||||
<GSelect<SlackChannel>
|
||||
showSearch
|
||||
className={cx('select', 'control')}
|
||||
modelName="slackChannelStore"
|
||||
items={slackChannelStore.items}
|
||||
fetchItemsFn={slackChannelStore.updateItems}
|
||||
fetchItemFn={slackChannelStore.updateItem}
|
||||
getSearchResult={slackChannelStore.getSearchResult}
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Slack Channel"
|
||||
value={store.organizationStore.currentOrganization?.slack_channel?.id}
|
||||
value={organizationStore.currentOrganization?.slack_channel?.id}
|
||||
onChange={this.handleSlackChannelChange}
|
||||
nullItemName={PRIVATE_CHANNEL_NAME}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ describe('rootBaseStore', () => {
|
|||
});
|
||||
isUserActionAllowed.mockReturnValueOnce(true);
|
||||
PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null);
|
||||
rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser;
|
||||
Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser });
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
|
@ -224,7 +224,7 @@ describe('rootBaseStore', () => {
|
|||
});
|
||||
isUserActionAllowed.mockReturnValueOnce(true);
|
||||
PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null);
|
||||
rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser;
|
||||
Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser });
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
|
@ -300,7 +300,7 @@ describe('rootBaseStore', () => {
|
|||
version: 'asdfasdf',
|
||||
license: 'asdfasdf',
|
||||
});
|
||||
rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser;
|
||||
Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser });
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
|
@ -329,7 +329,7 @@ describe('rootBaseStore', () => {
|
|||
license: 'asdfasdf',
|
||||
});
|
||||
PluginState.updatePluginStatus = jest.fn().mockResolvedValueOnce(updatePluginStatusError);
|
||||
rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser;
|
||||
Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser });
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import qs from 'query-string';
|
|||
import { OnCallAppPluginMeta } from 'types';
|
||||
|
||||
import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertReceiveChannelFiltersStore } from 'models/alert_receive_channel_filters/alert_receive_channel_filters';
|
||||
import { AlertGroupStore } from 'models/alertgroup/alertgroup';
|
||||
import { ApiTokenStore } from 'models/api_token/api_token';
|
||||
|
|
@ -31,6 +30,7 @@ import { TimezoneStore } from 'models/timezone/timezone';
|
|||
import { UserStore } from 'models/user/user';
|
||||
import { UserGroupStore } from 'models/user_group/user_group';
|
||||
import { makeRequest } from 'network/network';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { PluginState } from 'state/plugin/plugin';
|
||||
import { retryFailingPromises } from 'utils/async';
|
||||
|
|
@ -71,7 +71,7 @@ export class RootBaseStore {
|
|||
initialQuery = qs.parse(window.location.search);
|
||||
|
||||
@observable
|
||||
selectedAlertReceiveChannel?: AlertReceiveChannel['id'];
|
||||
selectedAlertReceiveChannel?: ApiSchemas['AlertReceiveChannel']['id'];
|
||||
|
||||
@observable
|
||||
features?: { [key: string]: boolean };
|
||||
|
|
@ -141,7 +141,7 @@ export class RootBaseStore {
|
|||
Promise.all([
|
||||
this.userStore.updateNotificationPolicyOptions(),
|
||||
this.userStore.updateNotifyByOptions(),
|
||||
this.alertReceiveChannelStore.updateAlertReceiveChannelOptions(),
|
||||
this.alertReceiveChannelStore.fetchAlertReceiveChannelOptions(),
|
||||
]);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,3 +12,18 @@ export interface TableColumn {
|
|||
export type PropertiesThatExtendsAnotherClass<OriginalObj, AnotherClass> = keyof {
|
||||
[Prop in keyof OriginalObj as OriginalObj[Prop] extends AnotherClass ? Prop : never]: unknown;
|
||||
};
|
||||
|
||||
// IfEquals, WritableKeys, ReadonlyKeys based on https://stackoverflow.com/a/49579497/4931398
|
||||
export type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
|
||||
? A
|
||||
: B;
|
||||
|
||||
export type WritableKeys<T> = {
|
||||
[P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>;
|
||||
}[keyof T];
|
||||
|
||||
export type ReadonlyKeys<T> = {
|
||||
[P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, never, P>;
|
||||
}[keyof T];
|
||||
|
||||
export type OmitReadonlyMembers<T> = Omit<T, ReadonlyKeys<T>>;
|
||||
|
|
|
|||
|
|
@ -18,18 +18,18 @@ export class KeyValuePair<T = string | number> {
|
|||
}
|
||||
}
|
||||
|
||||
export const formatBackendError = (payload: string | Record<string, unknown>) =>
|
||||
typeof payload === 'string'
|
||||
? payload
|
||||
: Object.keys(payload)
|
||||
.map((key) => `${sentenceCase(key)}: ${payload[key]}`)
|
||||
.join('\n');
|
||||
|
||||
export function showApiError(error: any) {
|
||||
if (isNetworkError(error) && error.response && error.response.status >= 400 && error.response.status < 500) {
|
||||
const payload = error.response.data;
|
||||
const text =
|
||||
typeof payload === 'string'
|
||||
? payload
|
||||
: Object.keys(payload)
|
||||
.map((key) => `${sentenceCase(key)}: ${payload[key]}`)
|
||||
.join('\n');
|
||||
const text = formatBackendError(error.response.data);
|
||||
openErrorNotification(text);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue