commit
3ca0a5937c
10 changed files with 180 additions and 58 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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": (),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
setShiftPeriodDefaultValue(undefined);
|
||||
setRecurrenceNum(value);
|
||||
|
||||
if (!isLimitShiftEnabled) {
|
||||
if (!isLimitShiftEnabled && !isMaskedByWeekdays) {
|
||||
setShiftEnd(
|
||||
dayJSAddWithDSTFixed({
|
||||
baseDate: rotationStart,
|
||||
|
|
|
|||
|
|
@ -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')}>
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue