Merge pull request #4663 from grafana/dev

v1.8.3
This commit is contained in:
Vadim Stepanov 2024-07-11 17:59:22 +01:00 committed by GitHub
commit 3ca0a5937c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 180 additions and 58 deletions

View file

@ -27,6 +27,13 @@ runs:
# yamllint disable rule:line-length
run: |
echo filename="grafana-oncall${{ inputs.is_enterprise == 'true' && '-ee' || '' }}-app-${{ inputs.plugin_version_number }}.zip" >> $GITHUB_OUTPUT
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: "1.21.5"
- name: Install Mage
shell: bash
run: go install github.com/magefile/mage@v1.15.0
- name: Build, sign, and package plugin
shell: bash
working-directory: ${{ inputs.working_directory }}
@ -35,6 +42,7 @@ runs:
run: |
jq --arg v "${{ inputs.plugin_version_number }}" '.version=$v' package.json > package.new && mv package.new package.json && jq '.version' package.json;
yarn build
mage buildAll || true
yarn sign
if [ ! -f dist/MANIFEST.txt ]; then echo "Sign failed, MANIFEST.txt not created, aborting." && exit 1; fi
mv dist grafana-oncall-app

View file

@ -88,8 +88,11 @@ via the method configured in their user profile.
* `Notify Slack User Group` - send a notification to each member of a slack user group. These users will be notified
via the method configured in their user profile.
* `Trigger outgoing webhook` - trigger an [outgoing webhook].
* `Notify users one by one (round robin)` - each notification will be sent to a group of
users one by one, in sequential order in [round robin fashion](https://en.wikipedia.org/wiki/Round-robin_item_allocation).
* `Notify users one by one (round robin)` - notify users sequentially, cycling through users for **different alert groups**.
Example: if users A, B, and C are in the list, the first alert group notifies A, the second alert group notifies B, and
the third alert group notifies C. Note: users are sorted alphabetically by their username.
To notify multiple users **within the same alert group** until someone acknowledges, instead use `Notify users` policies with
`Wait` policies between them in the escalation chain.
* `Continue escalation if current time is in range` - continue escalation only if current
time is in specified range. It will wait for the specfied time to continue escalation.
Useful when you want to get escalation only during working hours

View file

@ -86,15 +86,18 @@ class InboundEmailWebhookView(AlertChannelDefiningMixin, APIView):
# First try envelope_recipient field.
# According to AnymailInboundMessage it's provided not by all ESPs.
if message.envelope_recipient:
try:
token, domain = message.envelope_recipient.split("@")
except ValueError:
logger.error(
f"get_integration_token_from_request: envelope_recipient field has unexpected format: {message.envelope_recipient}"
)
return None
if domain == live_settings.INBOUND_EMAIL_DOMAIN:
return token
recipients = message.envelope_recipient.split(",")
for recipient in recipients:
# if there is more than one recipient, the first matching the expected domain will be used
try:
token, domain = recipient.strip().split("@")
except ValueError:
logger.error(
f"get_integration_token_from_request: envelope_recipient field has unexpected format: {message.envelope_recipient}"
)
continue
if domain == live_settings.INBOUND_EMAIL_DOMAIN:
return token
else:
logger.info("get_integration_token_from_request: message.envelope_recipient is not present")
"""
@ -152,7 +155,10 @@ class InboundEmailWebhookView(AlertChannelDefiningMixin, APIView):
def get_sender_from_email_message(self, email: AnymailInboundMessage) -> str:
try:
sender = email.from_email.addr_spec
if isinstance(email.from_email, list):
sender = email.from_email[0].addr_spec
else:
sender = email.from_email.addr_spec
except AnymailInvalidAddress as e:
# wasn't able to parse email address from message, return raw value from "From" header
logger.warning(

View file

@ -1,4 +1,5 @@
import json
from textwrap import dedent
import pytest
from anymail.inbound import AnymailInboundMessage
@ -9,8 +10,19 @@ from rest_framework.test import APIClient
from apps.email.inbound import InboundEmailWebhookView
@pytest.mark.parametrize(
"recipients,expected",
[
("{token}@example.com", status.HTTP_200_OK),
("{token}@example.com, another@example.com", status.HTTP_200_OK),
("{token}@example.com, another@example.com", status.HTTP_200_OK),
("{token}@other.com, {token}@example.com", status.HTTP_400_BAD_REQUEST),
],
)
@pytest.mark.django_db
def test_amazon_ses_provider_load(settings, make_organization_and_user_with_token, make_alert_receive_channel):
def test_amazon_ses_provider_load(
settings, make_organization_and_user_with_token, make_alert_receive_channel, recipients, expected
):
settings.INBOUND_EMAIL_ESP = "amazon_ses"
settings.INBOUND_EMAIL_DOMAIN = "example.com"
@ -19,10 +31,10 @@ def test_amazon_ses_provider_load(settings, make_organization_and_user_with_toke
organization, _, token = make_organization_and_user_with_token()
_ = make_alert_receive_channel(organization, token=dummy_channel_token)
recipient = f"{dummy_channel_token}@example.com"
recipients = recipients.format(token=dummy_channel_token)
mime = f"""From: sender@example.com
Subject: Dummy email message
To: {recipient}
To: {recipients}
Content-Type: text/plain
Hello!
@ -30,7 +42,7 @@ def test_amazon_ses_provider_load(settings, make_organization_and_user_with_toke
message = {
"notificationType": "Received",
"receipt": {"action": {"type": "SNS"}, "recipients": [recipient]},
"receipt": {"action": {"type": "SNS"}, "recipients": recipients.split(",")},
"content": mime,
}
@ -54,7 +66,62 @@ def test_amazon_ses_provider_load(settings, make_organization_and_user_with_toke
HTTP_X_AMZ_SNS_MESSAGE_ID=dummy_sns_message_id,
)
assert response.status_code == status.HTTP_200_OK
assert response.status_code == expected
@pytest.mark.parametrize(
"recipients,expected",
[
("{token}@example.com", status.HTTP_200_OK),
("{token}@example.com, another@example.com", status.HTTP_200_OK),
("{token}@example.com, another@example.com", status.HTTP_200_OK),
("{token}@other.com, {token}@example.com", status.HTTP_200_OK),
("{token}@other.com, {token}@another.com", status.HTTP_400_BAD_REQUEST),
],
)
@pytest.mark.django_db
def test_mailgun_provider_load(
settings, make_organization_and_user_with_token, make_alert_receive_channel, recipients, expected
):
settings.INBOUND_EMAIL_ESP = "mailgun"
settings.INBOUND_EMAIL_DOMAIN = "example.com"
settings.INBOUND_EMAIL_WEBHOOK_SECRET = "secret"
dummy_channel_token = "dummy-channel-token"
organization, _, token = make_organization_and_user_with_token()
_ = make_alert_receive_channel(organization, token=dummy_channel_token)
recipients = recipients.format(token=dummy_channel_token)
raw_event = {
"token": "06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0",
"timestamp": "1461261330",
"signature": "dbb05e62be402448b36ffb81f6abfb888273c95617aa077b4da355b25bab3670",
"recipient": "{recipients}".format(recipients=recipients),
"sender": "envelope-from@example.org",
"body-mime": dedent(
"""\
From: sender@example.com
Subject: Dummy email message
To: {recipients}
Content-Type: text/plain
Hello!
--94eb2c05e174adb140055b6339c5--
""".format(
recipients=recipients
)
),
}
client = APIClient()
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=raw_event,
HTTP_AUTHORIZATION=token,
)
assert response.status_code == expected
@pytest.mark.parametrize(

View file

@ -594,7 +594,7 @@ START_SYNC_ORG_WITH_CHATOPS_PROXY_ENABLED = getenv_boolean("START_SYNC_ORG_WITH_
if FEATURE_MULTIREGION_ENABLED and START_SYNC_ORG_WITH_CHATOPS_PROXY_ENABLED:
CELERY_BEAT_SCHEDULE["start_sync_org_with_chatops_proxy"] = {
"task": "apps.chatops_proxy.tasks.start_sync_org_with_chatops_proxy",
"schedule": crontab(hour="*/24"), # Every 24 hours, feel free to adjust
"schedule": crontab(minute=0, hour=12), # Execute every day at noon
"args": (),
}

View file

@ -1,31 +1,42 @@
import semver from 'semver';
import { scheduleViewToDaysInOneRow } from 'models/schedule/schedule.helpers';
import { ScheduleView } from 'models/schedule/schedule.types';
import { HTML_ID } from 'utils/DOM';
import { expect, test } from '../fixtures';
import { expect, Page, test } from '../fixtures';
import { generateRandomValue } from '../utils/forms';
import { createOnCallSchedule } from '../utils/schedule';
test.skip('schedule view (week/2 weeks/month) toggler works', async ({ adminRolePage }) => {
const getNumberOfWeekdaysInFinalSchedule = async (page: Page) =>
await page.locator(`#${HTML_ID.SCHEDULE_FINAL}`).getByTestId('schedule-weekday').count();
const getScheduleViewRadioButtonLocator = (page: Page, view: ScheduleView) =>
page
.getByTestId('schedule-view-picker')
[semver.lt(process.env.CURRENT_GRAFANA_VERSION, '10.2.0') ? 'getByText' : 'getByLabel'](view, { exact: true });
test('schedule view (week/2 weeks/month) toggler works', async ({ adminRolePage }) => {
const { page, userName } = adminRolePage;
const onCallScheduleName = generateRandomValue();
await createOnCallSchedule(page, onCallScheduleName, userName);
// ScheduleView.OneWeek is selected by default
expect(await page.getByLabel(ScheduleView.OneWeek, { exact: true }).isChecked()).toBe(true);
expect(await getScheduleViewRadioButtonLocator(page, ScheduleView.OneWeek).isChecked()).toBe(true);
expect(await page.locator(`#${HTML_ID.SCHEDULE_FINAL} .TEST_weekday`).count()).toStrictEqual(
expect(await getNumberOfWeekdaysInFinalSchedule(page)).toStrictEqual(
scheduleViewToDaysInOneRow[ScheduleView.OneWeek]
);
await page.getByLabel(ScheduleView.TwoWeeks, { exact: true }).click();
expect(await page.getByLabel(ScheduleView.TwoWeeks, { exact: true }).isChecked()).toBe(true);
expect(await page.locator(`#${HTML_ID.SCHEDULE_FINAL} .TEST_weekday`).count()).toStrictEqual(
await getScheduleViewRadioButtonLocator(page, ScheduleView.TwoWeeks).click();
await page.waitForTimeout(1000);
expect(await getScheduleViewRadioButtonLocator(page, ScheduleView.TwoWeeks).isChecked()).toBe(true);
expect(await getNumberOfWeekdaysInFinalSchedule(page)).toStrictEqual(
scheduleViewToDaysInOneRow[ScheduleView.TwoWeeks]
);
await page.getByLabel(ScheduleView.OneMonth, { exact: true }).click();
expect(await page.getByLabel(ScheduleView.OneMonth, { exact: true }).isChecked()).toBe(true);
expect(await page.locator(`#${HTML_ID.SCHEDULE_FINAL} .TEST_weekday`).count()).toBeGreaterThanOrEqual(28);
await getScheduleViewRadioButtonLocator(page, ScheduleView.OneMonth).click();
await page.waitForTimeout(1000);
expect(await getScheduleViewRadioButtonLocator(page, ScheduleView.OneMonth).isChecked()).toBe(true);
expect(await getNumberOfWeekdaysInFinalSchedule(page)).toBeGreaterThanOrEqual(28);
});

View file

@ -337,7 +337,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
setShiftPeriodDefaultValue(undefined);
setRecurrenceNum(value);
if (!isLimitShiftEnabled) {
if (!isLimitShiftEnabled && !isMaskedByWeekdays) {
setShiftEnd(
dayJSAddWithDSTFixed({
baseDate: rotationStart,

View file

@ -84,7 +84,8 @@ export const TimelineMarks: FC<TimelineMarksProps> = observer((props) => {
return (
<div
key={i}
className={cx('weekday', 'TEST_weekday', { 'weekday--weekEnd': isWeekend })}
data-testid="schedule-weekday"
className={cx('weekday', { 'weekday--weekEnd': isWeekend })}
style={{ width: `${100 / days}%` }}
>
<div className={cx('weekday-title')}>

View file

@ -356,28 +356,30 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
</div>
</HorizontalGroup>
<HorizontalGroup>
<RadioButtonGroup
options={[
{ label: ScheduleView.OneWeek, value: ScheduleView.OneWeek },
{ label: ScheduleView.TwoWeeks, value: ScheduleView.TwoWeeks },
{ label: ScheduleView.OneMonth, value: ScheduleView.OneMonth },
]}
value={scheduleView}
onChange={(value) => {
scheduleStore.setScheduleView(value);
if (value === ScheduleView.OneMonth) {
timezoneStore.setCalendarStartDate(
getCalendarStartDate(
timezoneStore.calendarStartDate.endOf('isoWeek').startOf('month'),
value,
timezoneStore.selectedTimezoneOffset
)
);
}
<div data-testid="schedule-view-picker">
<RadioButtonGroup
options={[
{ label: ScheduleView.OneWeek, value: ScheduleView.OneWeek },
{ label: ScheduleView.TwoWeeks, value: ScheduleView.TwoWeeks },
{ label: ScheduleView.OneMonth, value: ScheduleView.OneMonth },
]}
value={scheduleView}
onChange={(value) => {
scheduleStore.setScheduleView(value);
if (value === ScheduleView.OneMonth) {
timezoneStore.setCalendarStartDate(
getCalendarStartDate(
timezoneStore.calendarStartDate.endOf('isoWeek').startOf('month'),
value,
timezoneStore.selectedTimezoneOffset
)
);
}
scheduleStore.refreshEvents(scheduleId);
}}
/>
scheduleStore.refreshEvents(scheduleId);
}}
/>
</div>
<ScheduleFilters
value={filters}
onChange={(value) => this.setState({ filters: value })}

View file

@ -7085,9 +7085,9 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-loops@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.3.tgz#ce96adb86d07e7bf9b4822ab9c6fac9964981f75"
integrity sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==
version "1.1.4"
resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.4.tgz#61bc77d518c0af5073a638c6d9d5c7683f069ce2"
integrity sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==
fast-safe-stringify@^2.0.7:
version "2.1.1"
@ -13469,7 +13469,16 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -13587,7 +13596,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -13608,6 +13617,13 @@ strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -14948,8 +14964,7 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
name wrap-ansi-cjs
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -14967,6 +14982,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"