From 152b1b1c586fbfba4ba504a03767921d306658ee Mon Sep 17 00:00:00 2001 From: Ravishankar Date: Fri, 10 Nov 2023 22:17:11 +0530 Subject: [PATCH 01/15] fix(3093) Return timezone field of the user via public API (#3311) # What this PR does Return the `timezone` field for the users GET API call ## Which issue(s) this PR fixes Closes #3093 ## 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: Joey Orlando --- CHANGELOG.md | 6 ++++++ docs/sources/oncall-api-reference/users.md | 7 +++++-- engine/apps/public_api/serializers/users.py | 3 ++- engine/apps/public_api/tests/test_users.py | 3 +++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37d39e75..b1a5af5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added user timezone field to the users public API response ([#3311](https://github.com/grafana/oncall/pull/3311)) + ## v1.3.57 (2023-11-10) ### Fixed diff --git a/docs/sources/oncall-api-reference/users.md b/docs/sources/oncall-api-reference/users.md index b3e58821..04adcf1d 100644 --- a/docs/sources/oncall-api-reference/users.md +++ b/docs/sources/oncall-api-reference/users.md @@ -28,7 +28,8 @@ The above command returns JSON structured in the following way: } ], "username": "alex", - "role": "admin" + "role": "admin", + "timezone": "UTC" } ``` @@ -45,6 +46,7 @@ Use `{{API_URL}}/api/v1/users/current` to retrieve the current user. | `slack` | Yes/org | List of user IDs from connected Slack. User linking key is e-mail. | | `username` | Yes/org | User username | | `role` | No | One of: `user`, `observer`, `admin`. | +| `timezone` | No | timezone of the user one of [time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). | # List Users @@ -73,7 +75,8 @@ The above command returns JSON structured in the following way: } ], "username": "alex", - "role": "admin" + "role": "admin", + "timezone": "UTC" } ], "current_page_number": 1, diff --git a/engine/apps/public_api/serializers/users.py b/engine/apps/public_api/serializers/users.py index 6b0f8f26..4c3df2f4 100644 --- a/engine/apps/public_api/serializers/users.py +++ b/engine/apps/public_api/serializers/users.py @@ -53,7 +53,8 @@ class UserSerializer(serializers.ModelSerializer, EagerLoadingMixin): class Meta: model = User - fields = ["id", "email", "slack", "username", "role", "is_phone_number_verified"] + fields = ["id", "email", "slack", "username", "role", "is_phone_number_verified", "timezone"] + read_only_fields = ["timezone"] @staticmethod def get_role(obj): diff --git a/engine/apps/public_api/tests/test_users.py b/engine/apps/public_api/tests/test_users.py index fc77fcd4..433ac485 100644 --- a/engine/apps/public_api/tests/test_users.py +++ b/engine/apps/public_api/tests/test_users.py @@ -34,6 +34,7 @@ def test_get_user( "username": user.username, "role": "admin", "is_phone_number_verified": False, + "timezone": user.timezone, } assert response.status_code == status.HTTP_200_OK @@ -72,6 +73,7 @@ def test_get_users_list( "username": user_1.username, "role": "admin", "is_phone_number_verified": False, + "timezone": user_1.timezone, }, { "id": user_2.public_primary_key, @@ -80,6 +82,7 @@ def test_get_users_list( "username": user_2.username, "role": "admin", "is_phone_number_verified": False, + "timezone": user_2.timezone, }, ], "current_page_number": 1, From 37160806ca0b7ca0f829de70c033699d236da2cf Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 10 Nov 2023 11:54:22 -0500 Subject: [PATCH 02/15] Direct paging integrations table (#3290) # What this PR does Closes https://github.com/grafana/oncall/issues/3119 Closes https://github.com/grafana/oncall-private/issues/2061 ## Which issue(s) this PR fixes ## 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: Dominik --- CHANGELOG.md | 6 +- .../api/tests/test_alert_receive_channel.py | 23 ++ .../apps/api/views/alert_receive_channel.py | 3 + grafana-plugin/.eslintrc.js | 1 + .../e2e-tests/integrations/heartbeat.test.ts | 4 +- .../integrations/integrationsTable.test.ts | 47 ++++ .../integrations/maintenanceMode.test.ts | 2 +- .../e2e-tests/utils/escalationChain.ts | 15 +- .../e2e-tests/utils/integrations.ts | 39 +++- grafana-plugin/package.json | 2 + .../src/containers/Labels/LabelsFilter.tsx | 5 - .../RemoteFilters/RemoteFilters.tsx | 8 +- .../alert_receive_channel.ts | 14 +- .../alert_receive_channel.types.ts | 8 + .../integrations/Integrations.module.scss | 8 + .../src/pages/integrations/Integrations.tsx | 206 +++++++++++++----- 16 files changed, 311 insertions(+), 80 deletions(-) create mode 100644 grafana-plugin/e2e-tests/integrations/integrationsTable.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b1a5af5e..4065fa4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added user timezone field to the users public API response ([#3311](https://github.com/grafana/oncall/pull/3311)) +- Added user timezone field to the users public API response ([#3311](https://github.com/grafana/oncall/pull/3311)) + +### Changed + +- Split Integrations table into Connections and Direct Paging tabs ([#3290](https://github.com/grafana/oncall/pull/3290)) ## v1.3.57 (2023-11-10) diff --git a/engine/apps/api/tests/test_alert_receive_channel.py b/engine/apps/api/tests/test_alert_receive_channel.py index 04003de1..194a5645 100644 --- a/engine/apps/api/tests/test_alert_receive_channel.py +++ b/engine/apps/api/tests/test_alert_receive_channel.py @@ -33,6 +33,29 @@ def test_get_alert_receive_channel(alert_receive_channel_internal_api_setup, mak assert response.status_code == status.HTTP_200_OK +@pytest.mark.django_db +def test_get_alert_receive_channel_by_integration_ne( + make_organization_and_user_with_plugin_token, make_user_auth_headers, make_alert_receive_channel +): + organization, user, token = make_organization_and_user_with_plugin_token() + + make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA) + make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING) + make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING) + + client = APIClient() + url = f"{reverse('api-internal:alert_receive_channel-list')}?integration_ne={AlertReceiveChannel.INTEGRATION_DIRECT_PAGING}" + + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + results = response.json()["results"] + + assert response.status_code == status.HTTP_200_OK + assert len(results) == 2 + + for result in results: + assert result["integration"] != AlertReceiveChannel.INTEGRATION_DIRECT_PAGING + + @pytest.mark.django_db @pytest.mark.parametrize( "query_param,should_be_unpaginated", diff --git a/engine/apps/api/views/alert_receive_channel.py b/engine/apps/api/views/alert_receive_channel.py index 14e1a9ce..aa79da6d 100644 --- a/engine/apps/api/views/alert_receive_channel.py +++ b/engine/apps/api/views/alert_receive_channel.py @@ -43,6 +43,9 @@ class AlertReceiveChannelFilter(ByTeamModelFieldFilterMixin, filters.FilterSet): choices=AlertReceiveChannel.MAINTENANCE_MODE_CHOICES, method="filter_maintenance_mode" ) integration = filters.MultipleChoiceFilter(choices=AlertReceiveChannel.INTEGRATION_CHOICES) + integration_ne = filters.MultipleChoiceFilter( + choices=AlertReceiveChannel.INTEGRATION_CHOICES, field_name="integration", exclude=True + ) team = TeamModelMultipleChoiceFilter() class Meta: diff --git a/grafana-plugin/.eslintrc.js b/grafana-plugin/.eslintrc.js index 92831e8c..23cbc3ab 100644 --- a/grafana-plugin/.eslintrc.js +++ b/grafana-plugin/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { 'newlines-between': 'always', }, ], + 'no-console': ['warn', { allow: ['warn', 'error'] }], 'no-unused-vars': [ 'warn', { diff --git a/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts b/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts index 56702636..202f1832 100644 --- a/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts +++ b/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts @@ -15,7 +15,7 @@ test.describe("updating an integration's heartbeat interval works", async () => }; test('change heartbeat interval', async ({ adminRolePage: { page } }) => { - await createIntegration(page, generateRandomValue()); + await createIntegration({ page, integrationName: generateRandomValue() }); await _openHeartbeatSettingsForm(page); @@ -43,7 +43,7 @@ test.describe("updating an integration's heartbeat interval works", async () => }); test('send heartbeat', async ({ adminRolePage: { page } }) => { - await createIntegration(page, generateRandomValue()); + await createIntegration({ page, integrationName: generateRandomValue() }); await _openHeartbeatSettingsForm(page); diff --git a/grafana-plugin/e2e-tests/integrations/integrationsTable.test.ts b/grafana-plugin/e2e-tests/integrations/integrationsTable.test.ts new file mode 100644 index 00000000..b2ad52ad --- /dev/null +++ b/grafana-plugin/e2e-tests/integrations/integrationsTable.test.ts @@ -0,0 +1,47 @@ +import { test, expect } from '../fixtures'; +import { generateRandomValue } from '../utils/forms'; +import { createIntegration } from '../utils/integrations'; + +test('Integrations table shows data in Connections and Direct Paging tabs', async ({ adminRolePage: { page } }) => { + // // Create 2 integrations that are not Direct Paging + const ID = generateRandomValue(); + const WEBHOOK_INTEGRATION_NAME = `Webhook-${ID}`; + const ALERTMANAGER_INTEGRATION_NAME = `Alertmanager-${ID}`; + const DIRECT_PAGING_INTEGRATION_NAME = `Direct paging`; + + await createIntegration({ page, integrationSearchText: 'Webhook', integrationName: WEBHOOK_INTEGRATION_NAME }); + await page.getByRole('tab', { name: 'Tab Integrations' }).click(); + + await createIntegration({ + page, + integrationSearchText: 'Alertmanager', + shouldGoToIntegrationsPage: false, + integrationName: ALERTMANAGER_INTEGRATION_NAME, + }); + await page.getByRole('tab', { name: 'Tab Integrations' }).click(); + + // Create 1 Direct Paging integration if it doesn't exist + const integrationsTable = page.getByTestId('integrations-table'); + await page.getByRole('tab', { name: 'Tab Direct Paging' }).click(); + const isDirectPagingAlreadyCreated = await page.getByText('Direct paging').isVisible(); + if (!isDirectPagingAlreadyCreated) { + await createIntegration({ + page, + integrationSearchText: 'Direct paging', + shouldGoToIntegrationsPage: false, + integrationName: DIRECT_PAGING_INTEGRATION_NAME, + }); + } + await page.getByRole('tab', { name: 'Tab Integrations' }).click(); + + // By default Connections tab is opened and newly created integrations are visible except Direct Paging one + await expect(integrationsTable.getByText(WEBHOOK_INTEGRATION_NAME)).toBeVisible(); + await expect(integrationsTable.getByText(ALERTMANAGER_INTEGRATION_NAME)).toBeVisible(); + await expect(integrationsTable).not.toContainText(DIRECT_PAGING_INTEGRATION_NAME); + + // Then after switching to Direct Paging tab only Direct Paging integration is visible + await page.getByRole('tab', { name: 'Tab Direct Paging' }).click(); + await expect(integrationsTable.getByText(WEBHOOK_INTEGRATION_NAME)).not.toBeVisible(); + await expect(integrationsTable.getByText(ALERTMANAGER_INTEGRATION_NAME)).not.toBeVisible(); + await expect(integrationsTable).toContainText(DIRECT_PAGING_INTEGRATION_NAME); +}); diff --git a/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts b/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts index d4066a93..88ff3c6e 100644 --- a/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts +++ b/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts @@ -106,7 +106,7 @@ test.describe('maintenance mode works', () => { const integrationName = generateRandomValue(); await createEscalationChain(page, escalationChainName, EscalationStep.NotifyUsers, userName); - await createIntegration(page, integrationName); + await createIntegration({ page, integrationName }); await assignEscalationChainToIntegration(page, escalationChainName); await enableMaintenanceMode(page, maintenanceModeType); diff --git a/grafana-plugin/e2e-tests/utils/escalationChain.ts b/grafana-plugin/e2e-tests/utils/escalationChain.ts index c24c5afb..74115d43 100644 --- a/grafana-plugin/e2e-tests/utils/escalationChain.ts +++ b/grafana-plugin/e2e-tests/utils/escalationChain.ts @@ -32,7 +32,7 @@ export const createEscalationChain = async ( await page.locator('text=Loading...').waitFor({ state: 'detached' }); // open the create escalation chain modal - (await page.waitForSelector('text=New Escalation Chain')).click(); + (await page.waitForSelector('text=/New Escalation Chain/i')).click(); // fill in the name input await fillInInput(page, 'div[data-testid="create-escalation-chain-name-input-modal"] >> input', escalationChainName); @@ -44,7 +44,10 @@ export const createEscalationChain = async ( if (escalationStep) { // add an escalation step await selectDropdownValue({ - page, selectType: 'grafanaSelect', placeholderText: 'Add escalation step...', value: escalationStep, + page, + selectType: 'grafanaSelect', + placeholderText: 'Add escalation step...', + value: escalationStep, }); // toggle important @@ -52,13 +55,15 @@ export const createEscalationChain = async ( await selectDropdownValue({ page, selectType: 'grafanaSelect', - placeholderText: "Default", - value: "Important", + placeholderText: 'Default', + value: 'Important', }); } // select the escalation step value (e.g. user or schedule) - if (escalationStepValue) {await selectEscalationStepValue(page, escalationStep, escalationStepValue);} + if (escalationStepValue) { + await selectEscalationStepValue(page, escalationStep, escalationStepValue); + } } }; diff --git a/grafana-plugin/e2e-tests/utils/integrations.ts b/grafana-plugin/e2e-tests/utils/integrations.ts index 8c91598e..4a45211f 100644 --- a/grafana-plugin/e2e-tests/utils/integrations.ts +++ b/grafana-plugin/e2e-tests/utils/integrations.ts @@ -1,25 +1,40 @@ import { Page } from '@playwright/test'; -import { clickButton, selectDropdownValue } from './forms'; +import { clickButton, generateRandomValue, selectDropdownValue } from './forms'; import { goToOnCallPage } from './navigation'; -const CREATE_INTEGRATION_MODAL_TEST_ID_SELECTOR = 'div[data-testid="create-integration-modal"]'; - export const openCreateIntegrationModal = async (page: Page): Promise => { - // go to the integrations page - await goToOnCallPage(page, 'integrations'); - // open the create integration modal - (await page.waitForSelector('text=New integration')).click(); + await page.getByRole('button', { name: 'New integration' }).click(); // wait for it to pop up - await page.waitForSelector(CREATE_INTEGRATION_MODAL_TEST_ID_SELECTOR); + await page.getByTestId('create-integration-modal').waitFor(); }; -export const createIntegration = async (page: Page, integrationName: string): Promise => { +export const createIntegration = async ({ + page, + integrationName = `integration-${generateRandomValue()}`, + integrationSearchText = 'Webhook', + shouldGoToIntegrationsPage = true, +}: { + page: Page; + integrationName?: string; + integrationSearchText?: string; + shouldGoToIntegrationsPage?: boolean; +}): Promise => { + if (shouldGoToIntegrationsPage) { + // go to the integrations page + await goToOnCallPage(page, 'integrations'); + } + await openCreateIntegrationModal(page); - // create a webhook integration - (await page.waitForSelector(`${CREATE_INTEGRATION_MODAL_TEST_ID_SELECTOR} >> text=Webhook`)).click(); + // create an integration + await page + .getByTestId('create-integration-modal') + .getByTestId('integration-display-name') + .filter({ hasText: integrationSearchText }) + .first() + .click(); // fill in the required inputs (await page.waitForSelector('input[name="verbal_name"]', { state: 'attached' })).fill(integrationName); @@ -55,7 +70,7 @@ export const createIntegrationAndSendDemoAlert = async ( integrationName: string, escalationChainName: string ): Promise => { - await createIntegration(page, integrationName); + await createIntegration({ page, integrationName }); await assignEscalationChainToIntegration(page, escalationChainName); await sendDemoAlert(page); }; diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index e3b7d1c1..a93cb29b 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -13,6 +13,8 @@ "test:silent": "jest --silent", "test:e2e": "yarn playwright test --grep-invert @expensive", "test:e2e-expensive": "yarn playwright test --grep @expensive", + "test:e2e:watch": "yarn test:e2e --ui", + "test:e2e:gen": "yarn playwright codegen http://localhost:3000", "cleanup-e2e-results": "rm -rf playwright-report && rm -rf test-results", "e2e-show-report": "yarn playwright show-report", "dev": "grafana-toolkit plugin:dev", diff --git a/grafana-plugin/src/containers/Labels/LabelsFilter.tsx b/grafana-plugin/src/containers/Labels/LabelsFilter.tsx index 5c099987..667630c9 100644 --- a/grafana-plugin/src/containers/Labels/LabelsFilter.tsx +++ b/grafana-plugin/src/containers/Labels/LabelsFilter.tsx @@ -21,11 +21,8 @@ interface LabelsFilterProps { const LabelsFilter = observer((props: LabelsFilterProps) => { const { filterType, className, autoFocus, value: propsValue, onChange } = props; - const [value, setValue] = useState([]); - const [keys, setKeys] = useState([]); - const { alertGroupStore, labelsStore } = useStore(); const loadKeys = @@ -44,9 +41,7 @@ const LabelsFilter = observer((props: LabelsFilterProps) => { useEffect(() => { const keyValuePairs = (propsValue || []).map((k) => k.split(':')); - const promises = keyValuePairs.map(([keyId]) => loadValuesForKey(keyId)); - const fetchKeyValues = async () => await Promise.all(promises); fetchKeyValues().then((list) => { diff --git a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx index 936ec10c..13be43ab 100644 --- a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx +++ b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx @@ -44,6 +44,7 @@ interface RemoteFiltersProps extends WithStoreProps { defaultFilters?: FiltersValues; extraFilters?: (state, setState, onFiltersValueChange) => React.ReactNode; grafanaTeamStore: GrafanaTeamStore; + skipFilterOptionFn?: (filterOption: FilterOption) => boolean; } interface RemoteFiltersState { filterOptions?: FilterOption[]; @@ -86,11 +87,16 @@ class RemoteFilters extends Component { page, store: { filtersStore }, defaultFilters, + skipFilterOptionFn, } = this.props; - const filterOptions = await filtersStore.updateOptionsForPage(page); + let filterOptions = await filtersStore.updateOptionsForPage(page); const currentTablePageNum = parseInt(filtersStore.currentTablePageNum[page] || query.p || 1, 10); + if (skipFilterOptionFn) { + filterOptions = filterOptions.filter((option: FilterOption) => !skipFilterOptionFn(option)); + } + // set the current page from filters/query or default it to 1 filtersStore.setCurrentTablePageNum(page, currentTablePageNum); diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index 67afa9e6..972dccb6 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -21,6 +21,7 @@ import { AlertReceiveChannelCounters, ContactPoint, MaintenanceMode, + SupportedIntegrationFilters, } from './alert_receive_channel.types'; export class AlertReceiveChannelStore extends BaseStore { @@ -132,8 +133,17 @@ export class AlertReceiveChannelStore extends BaseStore { return results; } - async updatePaginatedItems(query: any = '', page = 1, updateCounters = false, invalidateFn = undefined) { - const filters = typeof query === 'string' ? { search: query } : query; + async updatePaginatedItems({ + filters, + page = 1, + updateCounters = false, + invalidateFn = undefined, + }: { + filters: SupportedIntegrationFilters; + page: number; + updateCounters: boolean; + invalidateFn: () => boolean; + }) { const { count, results, page_size } = await makeRequest(this.path, { params: { ...filters, page } }); if (invalidateFn?.()) { diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts index 81c1940f..ebb2fbba 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts @@ -62,3 +62,11 @@ export interface ContactPoint { contactPoint: string; notificationConnected: boolean; } + +export interface SupportedIntegrationFilters { + integration?: string[]; + integration_ne?: string[]; + team?: string[]; + label?: string[]; + searchTerm?: string; +} diff --git a/grafana-plugin/src/pages/integrations/Integrations.module.scss b/grafana-plugin/src/pages/integrations/Integrations.module.scss index a2d942f2..69f57cad 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.module.scss +++ b/grafana-plugin/src/pages/integrations/Integrations.module.scss @@ -7,6 +7,10 @@ width: 40px; } +.tabsBar { + margin-bottom: 24px; +} + .integrations-header { margin-bottom: 24px; right: 0; @@ -45,3 +49,7 @@ background: var(--cards-background); } } + +.goToDirectPagingAlert { + margin-top: 24px; +} diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index d67af530..205d4eeb 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -1,7 +1,18 @@ import React from 'react'; import { LabelTag } from '@grafana/labels'; -import { HorizontalGroup, Button, VerticalGroup, Icon, ConfirmModal, Tooltip } from '@grafana/ui'; +import { + HorizontalGroup, + Button, + VerticalGroup, + Icon, + ConfirmModal, + Tooltip, + Tab, + TabsBar, + TabContent, + Alert, +} from '@grafana/ui'; import cn from 'classnames/bind'; import { debounce } from 'lodash-es'; import { observer } from 'mobx-react'; @@ -28,7 +39,11 @@ import TeamName from 'containers/TeamName/TeamName'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { HeartIcon, HeartRedIcon } from 'icons'; import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel'; -import { AlertReceiveChannel, MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { + AlertReceiveChannel, + MaintenanceMode, + SupportedIntegrationFilters, +} from 'models/alert_receive_channel/alert_receive_channel.types'; import { LabelKeyValue } from 'models/label/label.types'; import IntegrationHelper from 'pages/integration/Integration.helper'; import { AppFeature } from 'state/features'; @@ -41,11 +56,29 @@ import { PAGE, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; import styles from './Integrations.module.scss'; +enum TabType { + Connections = 'connections', + DirectPaging = 'direct-paging', +} + +const TAB_QUERY_PARAM_KEY = 'tab'; + +const TABS = [ + { + label: 'Connections', + value: TabType.Connections, + }, + { + label: 'Direct Paging', + value: TabType.DirectPaging, + }, +]; + const cx = cn.bind(styles); const FILTERS_DEBOUNCE_MS = 500; interface IntegrationsState extends PageBaseState { - integrationsFilters: Record; + integrationsFilters: SupportedIntegrationFilters; alertReceiveChannelId?: AlertReceiveChannel['id'] | 'new'; confirmationModal: { isOpen: boolean; @@ -57,6 +90,7 @@ interface IntegrationsState extends PageBaseState { confirmationText?: string; onConfirm: () => void; }; + activeTab: TabType; } interface IntegrationsProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {} @@ -67,9 +101,10 @@ class Integrations extends React.Component super(props); this.state = { - integrationsFilters: { searchTerm: '' }, + integrationsFilters: { searchTerm: '', integration_ne: ['direct_paging'] }, errorData: initErrorDataState(), confirmationModal: undefined, + activeTab: props.query[TAB_QUERY_PARAM_KEY] || TabType.Connections, }; } @@ -81,14 +116,12 @@ class Integrations extends React.Component if (prevProps.match.params.id !== this.props.match.params.id) { this.parseQueryParams(); } + if (prevProps.query[TAB_QUERY_PARAM_KEY] !== this.props.query[TAB_QUERY_PARAM_KEY]) { + this.onTabChange(this.props.query[TAB_QUERY_PARAM_KEY] as TabType); + } } parseQueryParams = async () => { - this.setState((_prevState) => ({ - errorData: initErrorDataState(), - alertReceiveChannelId: undefined, - })); // reset state on query parse - const { store, match: { @@ -96,6 +129,11 @@ class Integrations extends React.Component }, } = this.props; + this.setState((_prevState) => ({ + errorData: initErrorDataState(), + alertReceiveChannelId: undefined, + })); // reset state on query parse + if (!id) { return; } @@ -114,24 +152,52 @@ class Integrations extends React.Component } }; + getFiltersBasedOnCurrentTab = () => ({ + ...this.state.integrationsFilters, + ...(this.state.activeTab === TabType.DirectPaging + ? { integration: ['direct_paging'] } + : { + integration_ne: ['direct_paging'], + integration: this.state.integrationsFilters.integration?.filter( + (integration) => integration !== 'direct_paging' + ), + }), + }); + update = () => { const { store } = this.props; - const { integrationsFilters } = this.state; const page = store.filtersStore.currentTablePageNum[PAGE.Integrations]; LocationHelper.update({ p: page }, 'partial'); - return store.alertReceiveChannelStore.updatePaginatedItems(integrationsFilters, page, false, () => - this.invalidateRequestFn(page) + return store.alertReceiveChannelStore.updatePaginatedItems({ + filters: this.getFiltersBasedOnCurrentTab(), + page, + updateCounters: false, + invalidateFn: () => this.invalidateRequestFn(page), + }); + }; + + onTabChange = (tab: TabType) => { + LocationHelper.update({ tab, integration: undefined, search: undefined }, 'partial'); + this.setState( + { + activeTab: tab, + }, + () => { + this.handleChangePage(1); + } ); }; render() { const { store, query } = this.props; - const { alertReceiveChannelId, confirmationModal } = this.state; + const { alertReceiveChannelId, confirmationModal, activeTab, integrationsFilters } = this.state; const { alertReceiveChannelStore } = store; const { count, results, page_size } = alertReceiveChannelStore.getPaginatedSearchResult(); + const isDirectPagingSelectedOnConnectionsTab = + activeTab === TabType.Connections && integrationsFilters.integration?.includes('direct_paging'); return ( <> @@ -158,27 +224,58 @@ class Integrations extends React.Component
- - + + {TABS.map(({ label, value }) => ( + this.onTabChange(value)} + /> + ))} + + + name === 'integration', + })} + /> + {isDirectPagingSelectedOnConnectionsTab && ( + + + They are in a separate tab now. Go to{' '} + + Direct Paging tab + {' '} + to view them. + + + )} + +
@@ -455,6 +552,7 @@ class Integrations extends React.Component getTableColumns = (hasFeatureFn) => { const { grafanaTeamStore, alertReceiveChannelStore } = this.props.store; + const isConnectionsTab = this.state.activeTab === TabType.Connections; const columns = [ { @@ -476,21 +574,24 @@ class Integrations extends React.Component key: 'datasource', render: (item: AlertReceiveChannel) => this.renderDatasource(item, alertReceiveChannelStore), }, + ...(isConnectionsTab + ? [ + { + width: '10%', + title: 'Maintenance', + key: 'maintenance', + render: (item: AlertReceiveChannel) => this.renderMaintenance(item), + }, + { + width: '5%', + title: 'Heartbeat', + key: 'heartbeat', + render: (item: AlertReceiveChannel) => this.renderHeartbeat(item), + }, + ] + : []), { - width: '10%', - title: 'Maintenance', - key: 'maintenance', - render: (item: AlertReceiveChannel) => this.renderMaintenance(item), - }, - { - width: '5%', - title: 'Heartbeat', - key: 'heartbeat', - render: (item: AlertReceiveChannel) => this.renderHeartbeat(item), - }, - - { - width: '15%', + width: isConnectionsTab ? '15%' : '30%', title: 'Team', render: (item: AlertReceiveChannel) => this.renderTeam(item, grafanaTeamStore.items), }, @@ -572,12 +673,15 @@ class Integrations extends React.Component applyFilters = async (isOnMount: boolean) => { const { store } = this.props; const { alertReceiveChannelStore } = store; - const { integrationsFilters } = this.state; - const newPage = isOnMount ? store.filtersStore.currentTablePageNum[PAGE.Integrations] : 1; return alertReceiveChannelStore - .updatePaginatedItems(integrationsFilters, newPage, false, () => this.invalidateRequestFn(newPage)) + .updatePaginatedItems({ + filters: this.getFiltersBasedOnCurrentTab(), + page: newPage, + updateCounters: false, + invalidateFn: () => this.invalidateRequestFn(newPage), + }) .then(() => { store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage; LocationHelper.update({ p: newPage }, 'partial'); From 8ddea0576e2c46d9c0609af0313c8fb9758963cc Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Fri, 10 Nov 2023 14:44:37 -0300 Subject: [PATCH 03/15] Add test ensuring ingestion works without db access (#3322) Handling alert payloads should work without db access (but still requires cached integrations information) --- engine/apps/integrations/tests/test_views.py | 109 +++++++++++++++++++ engine/apps/labels/tests/factories.py | 4 +- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/engine/apps/integrations/tests/test_views.py b/engine/apps/integrations/tests/test_views.py index d6545c3d..80274a62 100644 --- a/engine/apps/integrations/tests/test_views.py +++ b/engine/apps/integrations/tests/test_views.py @@ -2,11 +2,24 @@ from unittest.mock import call, patch import pytest from django.core.files.uploadedfile import SimpleUploadedFile +from django.db import OperationalError from django.urls import reverse +from pytest_django.plugin import _DatabaseBlocker from rest_framework import status from rest_framework.test import APIClient from apps.alerts.models import AlertReceiveChannel +from apps.integrations.mixins import AlertChannelDefiningMixin + + +class DatabaseBlocker(_DatabaseBlocker): + """Customize pytest_django db blocker to raise OperationalError exception.""" + + def _blocking_wrapper(*args, **kwargs): + __tracebackhide__ = True + __tracebackhide__ # Silence pyflakes + # mimic DB unavailable error + raise OperationalError("Database access disabled") @pytest.mark.django_db @@ -184,3 +197,99 @@ def test_integration_universal_endpoint_not_allow_files( assert response.status_code == status.HTTP_400_BAD_REQUEST assert not mock_create_alert.apply_async.called + + +@patch("apps.integrations.views.create_alert") +@pytest.mark.parametrize( + "integration_type", + [ + arc_type + for arc_type in AlertReceiveChannel.INTEGRATION_TYPES + if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"] + ], +) +@pytest.mark.django_db +def test_integration_universal_endpoint_works_without_db( + mock_create_alert, make_organization_and_user, make_alert_receive_channel, integration_type +): + organization, user = make_organization_and_user() + alert_receive_channel = make_alert_receive_channel( + organization=organization, + author=user, + integration=integration_type, + ) + + client = APIClient() + url = reverse( + "integrations:universal", + kwargs={"integration_type": integration_type, "alert_channel_key": alert_receive_channel.token}, + ) + + # populate cache + AlertChannelDefiningMixin().update_alert_receive_channel_cache() + + # disable DB access + with DatabaseBlocker().block(): + data = {"foo": "bar"} + response = client.post(url, data, format="json") + + assert response.status_code == status.HTTP_200_OK + + mock_create_alert.apply_async.assert_called_once_with( + [], + { + "title": None, + "message": None, + "image_url": None, + "link_to_upstream_details": None, + "alert_receive_channel_pk": alert_receive_channel.pk, + "integration_unique_data": None, + "raw_request_data": data, + }, + ) + + +@patch("apps.integrations.views.create_alertmanager_alerts") +@pytest.mark.django_db +def test_integration_grafana_endpoint_without_db_has_alerts( + mock_create_alertmanager_alerts, settings, make_organization_and_user, make_alert_receive_channel +): + settings.DEBUG = False + + integration_type = "grafana" + organization, user = make_organization_and_user() + alert_receive_channel = make_alert_receive_channel( + organization=organization, + author=user, + integration=integration_type, + ) + + client = APIClient() + url = reverse("integrations:grafana", kwargs={"alert_channel_key": alert_receive_channel.token}) + + data = { + "alerts": [ + { + "foo": 123, + }, + { + "foo": 456, + }, + ] + } + + # populate cache + AlertChannelDefiningMixin().update_alert_receive_channel_cache() + + # disable DB access + with DatabaseBlocker().block(): + response = client.post(url, data, format="json") + + assert response.status_code == status.HTTP_200_OK + + mock_create_alertmanager_alerts.apply_async.assert_has_calls( + [ + call((alert_receive_channel.pk, data["alerts"][0])), + call((alert_receive_channel.pk, data["alerts"][1])), + ] + ) diff --git a/engine/apps/labels/tests/factories.py b/engine/apps/labels/tests/factories.py index 094fe0d3..5f910989 100644 --- a/engine/apps/labels/tests/factories.py +++ b/engine/apps/labels/tests/factories.py @@ -10,7 +10,7 @@ from common.utils import UniqueFaker class LabelKeyFactory(factory.DjangoModelFactory): - id = UniqueFaker("sentence", nb_words=3) + id = UniqueFaker("pystr", max_chars=36) name = UniqueFaker("sentence", nb_words=3) class Meta: @@ -18,7 +18,7 @@ class LabelKeyFactory(factory.DjangoModelFactory): class LabelValueFactory(factory.DjangoModelFactory): - id = UniqueFaker("sentence", nb_words=3) + id = UniqueFaker("pystr", max_chars=36) name = UniqueFaker("sentence", nb_words=3) class Meta: From 6ca7c441d96b0ba7a641d2eae7d5cc0616e917d4 Mon Sep 17 00:00:00 2001 From: Tomica-G <56955081+Tomica-G@users.noreply.github.com> Date: Fri, 10 Nov 2023 21:37:04 +0100 Subject: [PATCH 04/15] Update _index.md (#3088) # What this PR does Fixed a typo in the line #93 (changed `doen't` to `doesn't`) ## Which issue(s) this PR fixes ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [* ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- docs/sources/integrations/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/integrations/_index.md b/docs/sources/integrations/_index.md index fe9ed57a..1b0df9cf 100644 --- a/docs/sources/integrations/_index.md +++ b/docs/sources/integrations/_index.md @@ -90,7 +90,7 @@ will be sent to users. #### Heartbeat monitoring An OnCall heartbeat acts as a healthcheck for alert group monitoring. You can configure you monitoring to regularly send alerts -to the heartbeat endpoint. If OnCall doen't receive one of these alerts, it will create an new alert group and escalate it +to the heartbeat endpoint. If OnCall doesn't receive one of these alerts, it will create an new alert group and escalate it 1. Go to Integration page and click **Three dots** 1. Select **Heartbeat Settings** From 6fa4df0afed95e1afc25cc45c42e26d23d9763c7 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Sat, 11 Nov 2023 11:11:51 -0700 Subject: [PATCH 05/15] Forward headers for Amazon SNS (#3326) # What this PR does Forward headers for Amazon SNS when forwarding requests for moved organizations. Previous [PR](https://github.com/grafana/oncall/pull/3315) missed this since the test did not check mocked make_request for headers. ## Which issue(s) this PR fixes ## 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) --- CHANGELOG.md | 4 +++ engine/apps/user_management/middlewares.py | 5 +++ .../apps/user_management/tests/test_region.py | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4065fa4c..abcb3298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Split Integrations table into Connections and Direct Paging tabs ([#3290](https://github.com/grafana/oncall/pull/3290)) +### Fixed + +- Forward headers for Amazon SNS when organizations are moved @mderynck ([#3326](https://github.com/grafana/oncall/pull/3326)) + ## v1.3.57 (2023-11-10) ### Fixed diff --git a/engine/apps/user_management/middlewares.py b/engine/apps/user_management/middlewares.py index d9b65d08..10d5df1a 100644 --- a/engine/apps/user_management/middlewares.py +++ b/engine/apps/user_management/middlewares.py @@ -32,6 +32,11 @@ class OrganizationMovedMiddleware(MiddlewareMixin): if (v := request.META.get("HTTP_AUTHORIZATION", None)) is not None: headers["Authorization"] = v + if "amazon_sns" in request.path: + for k, v in request.META.items(): + if k.startswith("x-amz-sns-"): + headers[k] = v + response = self.make_request(request.method, url, headers, request.body) return HttpResponse(response.content, status=response.status_code) diff --git a/engine/apps/user_management/tests/test_region.py b/engine/apps/user_management/tests/test_region.py index 6331327c..bf6d640e 100644 --- a/engine/apps/user_management/tests/test_region.py +++ b/engine/apps/user_management/tests/test_region.py @@ -238,3 +238,36 @@ def test_user_schedule_export_token_raises_exception_organization_moved( assert False except OrganizationMovedException as e: assert e.organization == organization + + +@patch("apps.user_management.middlewares.OrganizationMovedMiddleware.make_request") +@pytest.mark.django_db +def test_organization_moved_middleware_amazon_sns_headers( + mocked_make_request, make_organization_and_region, make_alert_receive_channel +): + organization, region = make_organization_and_region() + organization.save() + + alert_receive_channel = make_alert_receive_channel( + organization=organization, + integration="amazon_sns", + ) + + expected_sns_headers = { + "x-amz-sns-subscription-arn": "arn:aws:sns:xxxxxxxxxx:467989492352:oncall-test:3aab6edb-0c5e-4fa9-b876-64409d1f6c63", + "x-amz-sns-topic-arn": "arn:aws:sns:xxxxxxxxxx:467989492352:oncall-test", + "x-amz-sns-message-id": "473efe1d-8ea4-5252-8124-a3d5ff7408c5", + "x-amz-sns-message-type": "Notification", + } + expected_message = bytes(f"Redirected to {region.oncall_backend_url}", "utf-8") + mocked_make_request.return_value = HttpResponse(expected_message, status=status.HTTP_200_OK) + + client = APIClient() + url = reverse("integrations:amazon_sns", kwargs={"alert_channel_key": alert_receive_channel.token}) + + data = {"value": "test"} + response = client.post(url, data, format="json", **expected_sns_headers) + assert mocked_make_request.called + assert expected_sns_headers.items() <= mocked_make_request.call_args.args[2].items() + assert response.content == expected_message + assert response.status_code == status.HTTP_200_OK From dcf08425ebb091e0165385fa889074d859deb091 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 13 Nov 2023 07:44:54 -0500 Subject: [PATCH 06/15] Fix few minor Slack connection issues (#3327) # What this PR does Closes https://github.com/grafana/oncall-private/issues/2289 - Fix issue where if you try connecting your Slack user to your OnCall user and the first time around you encounter an error (ex. connecting to the wrong Slack workspace), you will see the same error banner message despite a successful connection. Now we clear the session upon successful connection to ensure that you will not see any previously encountered errors. - Fix some alignment issues on the Slack connection buttons **Before** Screenshot 2023-11-10 at 15 07 48 Screenshot 2023-11-10 at 15 16 22 **After** Screenshot 2023-11-10 at 15 10 28 Screenshot 2023-11-10 at 15 16 42 - On the "User Info" user settings modal tab, render `display_name` instead of `slack_login`. Currently we prefix `@` before `slack_login`, which is a bit confusing as it makes you think that this is the handle you would use to `@` your user in Slack. `display_name` corresponds to the handle that would be used to `@` your user ## Checklist - [ ] 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) --- CHANGELOG.md | 1 + engine/apps/social_auth/pipeline.py | 5 +++++ .../containers/MobileAppConnection/MobileAppConnection.tsx | 7 +++++-- .../__snapshots__/MobileAppConnection.test.tsx.snap | 4 ++-- .../UserSettings/parts/connectors/SlackConnector.tsx | 2 +- .../UserSettings/parts/tabs/SlackTab/SlackTab.tsx | 6 ++++-- grafana-plugin/src/models/user/user.types.ts | 1 + .../tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx | 4 +++- 8 files changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abcb3298..a81e264f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fix issue where Slack user connection error message is sometimes shown despite successful connection by @joeyorlando ([#3327](https://github.com/grafana/oncall/pull/3327)) - Forward headers for Amazon SNS when organizations are moved @mderynck ([#3326](https://github.com/grafana/oncall/pull/3326)) ## v1.3.57 (2023-11-10) diff --git a/engine/apps/social_auth/pipeline.py b/engine/apps/social_auth/pipeline.py index 10acb47d..6eb6c621 100644 --- a/engine/apps/social_auth/pipeline.py +++ b/engine/apps/social_auth/pipeline.py @@ -57,6 +57,11 @@ def connect_user_to_slack(response, backend, strategy, user, organization, *args strategy.session[REDIRECT_FIELD_NAME] = url return HttpResponse(status=status.HTTP_400_BAD_REQUEST) + # at this point everything is correct and we can create the SlackUserIdentity + # be sure to clear any pre-existing sessions, in case the user previously enecountered errors we want + # to be sure to clear these so they do not see them again + strategy.session.flush() + slack_user_identity, _ = SlackUserIdentity.objects.get_or_create( slack_id=slack_user_id, slack_team_identity=slack_team_identity, diff --git a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx index 4e87938f..b1ae577a 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx +++ b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx @@ -152,7 +152,8 @@ const MobileAppConnection = observer(({ userPk }: Props) => { App connected - You can sync one application to your account. To setup new device please disconnect app first. + You can only sync one application to your account. To setup a new device, please disconnect the currently + connected device first.
@@ -168,7 +169,9 @@ const MobileAppConnection = observer(({ userPk }: Props) => { Sign In - Open Grafana IRM mobile application and scan this code to sync it with your account. + + Open the Grafana IRM mobile application and scan this code to sync it with your account. +
{isQRBlurry && } diff --git a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap index 1f46a2a3..74d48e6f 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap +++ b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap @@ -120,7 +120,7 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche - Open Grafana IRM mobile application and scan this code to sync it with your account. + Open the Grafana IRM mobile application and scan this code to sync it with your account.
- You can sync one application to your account. To setup new device please disconnect app first. + You can only sync one application to your account. To setup a new device, please disconnect the currently connected device first.
{ diff --git a/grafana-plugin/src/containers/UserSettings/parts/tabs/SlackTab/SlackTab.tsx b/grafana-plugin/src/containers/UserSettings/parts/tabs/SlackTab/SlackTab.tsx index d6d72f0b..ae11270f 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/tabs/SlackTab/SlackTab.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/tabs/SlackTab/SlackTab.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; -import { Button, VerticalGroup, Icon } from '@grafana/ui'; +import { Button, VerticalGroup, Icon, HorizontalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; import Block from 'components/GBlock/Block'; @@ -48,7 +48,9 @@ export const SlackTab = () => { diff --git a/grafana-plugin/src/models/user/user.types.ts b/grafana-plugin/src/models/user/user.types.ts index 6d3caae3..06a60c2d 100644 --- a/grafana-plugin/src/models/user/user.types.ts +++ b/grafana-plugin/src/models/user/user.types.ts @@ -34,6 +34,7 @@ export interface User extends BaseUser { unverified_phone_number?: string; slack_user_identity: { avatar: string; + display_name: string; name: string; slack_id: string; slack_login: string; diff --git a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx index bac6f633..e0540ec6 100644 --- a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx +++ b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx @@ -289,7 +289,9 @@ class SlackSettings extends Component { ) : ( {store.hasFeature(AppFeature.LiveSettings) && ( From b2dda2fc35961b87d4768946440b609cbcfca3d8 Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Mon, 13 Nov 2023 14:07:39 +0100 Subject: [PATCH 07/15] Exclude dark css vars when light theme is turned on (#3336) # What this PR does Fix styling when light theme is turned on via system preferences by excluding dark theme css vars in this case ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/3188 ## 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) --- CHANGELOG.md | 2 ++ grafana-plugin/src/assets/style/vars.css | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a81e264f..a27a1002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix issue where Slack user connection error message is sometimes shown despite successful connection by @joeyorlando ([#3327](https://github.com/grafana/oncall/pull/3327)) - Forward headers for Amazon SNS when organizations are moved @mderynck ([#3326](https://github.com/grafana/oncall/pull/3326)) +- Fix styling when light theme is turned on via system preferences + by excluding dark theme css vars in this case ([#3336](https://github.com/grafana/oncall/pull/3336)) ## v1.3.57 (2023-11-10) diff --git a/grafana-plugin/src/assets/style/vars.css b/grafana-plugin/src/assets/style/vars.css index c10443b3..17585703 100644 --- a/grafana-plugin/src/assets/style/vars.css +++ b/grafana-plugin/src/assets/style/vars.css @@ -71,7 +71,7 @@ --working-hours-shades-color-light: rgba(17, 18, 23, 0.04); } -.theme-dark { +.theme-dark:not(.theme-light) { --cards-background: var(--gray-9); --highlighted-row-bg: var(--gray-9); --disabled-button-color: hsla(0, 0%, 100%, 0.08); From 914a92cae84b3a941572fe9ebd7ba8ddfc8d13bb Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 13 Nov 2023 08:43:33 -0500 Subject: [PATCH 08/15] Improve performance on user search results in add responders dropdown (#3325) ## Which issue(s) this PR fixes Closes https://github.com/grafana/oncall/issues/3321 ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- .../AddRespondersPopup.module.scss | 4 +- .../AddRespondersPopup/AddRespondersPopup.tsx | 52 ++++++++++++++----- grafana-plugin/src/models/user/user.ts | 2 +- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.module.scss b/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.module.scss index 7b5f57ac..cfbe02c8 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.module.scss +++ b/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.module.scss @@ -1,4 +1,6 @@ .add-responders-dropdown { + max-height: 500px; + overflow: hidden; border: var(--border-medium); position: absolute; right: 16px; @@ -8,7 +10,7 @@ z-index: 10; } -.team-direct-paging-info-alert { +.info-alert { margin: 8px; } diff --git a/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.tsx b/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.tsx index 6e256c39..e1aea823 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.tsx +++ b/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.tsx @@ -10,6 +10,7 @@ import GTable from 'components/GTable/GTable'; import Text from 'components/Text/Text'; import { Alert as AlertType } from 'models/alertgroup/alertgroup.types'; import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; +import { PaginatedUsersResponse } from 'models/user/user'; import { UserCurrentlyOnCall } from 'models/user/user.types'; import { useStore } from 'state/useStore'; import { useDebouncedCallback, useOnClickOutside } from 'utils/hooks'; @@ -51,12 +52,11 @@ const AddRespondersPopup = observer( const [searchLoading, setSearchLoading] = useState(true); const [activeOption, setActiveOption] = useState(isCreateMode ? TabOptions.Teams : TabOptions.Users); const [teamSearchResults, setTeamSearchResults] = useState([]); - const [userSearchResults, setUserSearchResults] = useState([]); + const [onCallUserSearchResults, setOnCallUserSearchResults] = useState([]); + const [notOnCallUserSearchResults, setNotOnCallUserSearchResults] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const ref = useRef(); - const usersCurrentlyOnCall = userSearchResults.filter(({ is_currently_oncall }) => is_currently_oncall); - const usersNotCurrentlyOnCall = userSearchResults.filter(({ is_currently_oncall }) => !is_currently_oncall); useOnClickOutside(ref, () => { setVisible(false); @@ -97,11 +97,18 @@ const AddRespondersPopup = observer( ); const searchForUsers = useCallback(async () => { - /** - * specifying is_currently_oncall=all will tell the backend not to paginate the results - */ - const userResults = await userStore.search({ searchTerm, is_currently_oncall: 'all' }); - setUserSearchResults(userResults); + const _search = async (is_currently_oncall: boolean) => { + const response = await userStore.search>({ + searchTerm, + is_currently_oncall, + }); + return response.results; + }; + + const [onCallUserSearchResults, notOnCallUserSearchResults] = await Promise.all([_search(true), _search(false)]); + + setOnCallUserSearchResults(onCallUserSearchResults); + setNotOnCallUserSearchResults(notOnCallUserSearchResults); }, [searchTerm]); const searchForTeams = useCallback(async () => { @@ -153,9 +160,12 @@ const AddRespondersPopup = observer( useEffect(() => { if (existingPagedUsers.length > 0) { const existingPagedUserIds = existingPagedUsers.map(({ pk }) => pk); - setUserSearchResults((userSearchResults) => - userSearchResults.filter(({ pk }) => !existingPagedUserIds.includes(pk)) - ); + + const _filterUsers = (users: UserCurrentlyOnCall[]) => + users.filter(({ pk }) => !existingPagedUserIds.includes(pk)); + + setOnCallUserSearchResults(_filterUsers); + setNotOnCallUserSearchResults(_filterUsers); } }, [existingPagedUsers]); @@ -293,7 +303,7 @@ const AddRespondersPopup = observer( ) : ( <> - - + + We display a maximum of 100 users per category. Use the search bar above to refine results. You + can search by username, email, or team name. + + ) as any + } + /> + +
+ +
)}
diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index 1c658077..a3d10505 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -15,7 +15,7 @@ import { isUserActionAllowed, UserActions } from 'utils/authorization'; import { getTimezone, prepareForUpdate } from './user.helpers'; import { User } from './user.types'; -type PaginatedUsersResponse = { +export type PaginatedUsersResponse = { count: number; page_size: number; results: UT[]; From 13c72127d243aea6ad7646809a7ae996b089ada1 Mon Sep 17 00:00:00 2001 From: Matvey Kukuy Date: Tue, 14 Nov 2023 11:26:27 +0200 Subject: [PATCH 09/15] Grafana IRM -> Grafana OnCall (#3343) Small fix. --- .../src/containers/MobileAppConnection/MobileAppConnection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx index b1ae577a..9518a2b8 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx +++ b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx @@ -170,7 +170,7 @@ const MobileAppConnection = observer(({ userPk }: Props) => { Sign In - Open the Grafana IRM mobile application and scan this code to sync it with your account. + Open the Grafana OnCall mobile application and scan this code to sync it with your account.
From 4cff51e43c85d6d899bb6b3e340224820e53b35f Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 14 Nov 2023 12:42:07 +0000 Subject: [PATCH 10/15] fix jest snapshot (#3346) --- .../__snapshots__/MobileAppConnection.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap index 74d48e6f..63dd5bef 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap +++ b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap @@ -120,7 +120,7 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche - Open the Grafana IRM mobile application and scan this code to sync it with your account. + Open the Grafana OnCall mobile application and scan this code to sync it with your account.
Date: Tue, 14 Nov 2023 09:56:58 -0300 Subject: [PATCH 11/15] Enable filtering users by public primary key (#3339) Related to https://github.com/grafana/oncall/issues/3164 This will allow the following request to check if a user is currently on-call: `GET /api/internal/v1/users/?search=UCGEIXI1MR1NZ&is_currently_oncall=true` --- CHANGELOG.md | 1 + engine/apps/api/tests/test_user.py | 25 +++++++++++++++++++++++++ engine/apps/api/views/user.py | 1 + 3 files changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a27a1002..f06ebc9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added user timezone field to the users public API response ([#3311](https://github.com/grafana/oncall/pull/3311)) +- Allow filtering users by public primary key in internal API ([#3339](https://github.com/grafana/oncall/pull/3339)) ### Changed diff --git a/engine/apps/api/tests/test_user.py b/engine/apps/api/tests/test_user.py index 8e69ba8c..bc876c83 100644 --- a/engine/apps/api/tests/test_user.py +++ b/engine/apps/api/tests/test_user.py @@ -262,6 +262,31 @@ def test_list_users_filtered_by_granted_permission( assert user3.public_primary_key not in returned_user_pks +@pytest.mark.django_db +def test_list_users_filtered_by_public_primary_key( + make_organization, + make_user_for_organization, + make_token_for_organization, + make_user_auth_headers, +): + organization = make_organization() + admin_user = make_user_for_organization(organization) + user1 = make_user_for_organization(organization) + make_user_for_organization(organization) + _, token = make_token_for_organization(organization) + + client = APIClient() + url = reverse("api-internal:user-list") + + response = client.get( + f"{url}?search={user1.public_primary_key}", format="json", **make_user_auth_headers(admin_user, token) + ) + + assert response.status_code == status.HTTP_200_OK + returned_user_pks = [u["pk"] for u in response.json()["results"]] + assert returned_user_pks == [user1.public_primary_key] + + @pytest.mark.django_db def test_notification_chain_verbal( make_organization, diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index 6434d57a..7b3557cb 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -222,6 +222,7 @@ class UserView( "^slack_user_identity__cached_slack_login", "^slack_user_identity__cached_name", "^teams__name", + "=public_primary_key", ) filterset_class = UserFilter From d7d5c3aa284019f7ac7d97831a63972d8a2982ea Mon Sep 17 00:00:00 2001 From: Yulya Artyukhina Date: Tue, 14 Nov 2023 14:39:27 +0100 Subject: [PATCH 12/15] Fix acknowledge reminder (#3345) # What this PR does Fix acknowledge reminder: - check if organization was deleted - improve logging ## Which issue(s) this PR fixes ## 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) --- CHANGELOG.md | 1 + .../apps/alerts/tasks/acknowledge_reminder.py | 48 ++++++++++++------- .../alerts/tests/test_acknowledge_reminder.py | 40 ++++++++++++++++ 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f06ebc9c..e68637c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Forward headers for Amazon SNS when organizations are moved @mderynck ([#3326](https://github.com/grafana/oncall/pull/3326)) - Fix styling when light theme is turned on via system preferences by excluding dark theme css vars in this case ([#3336](https://github.com/grafana/oncall/pull/3336)) +- Fix issue when acknowledge reminder works for deleted organizations @Ferril ([#3345](https://github.com/grafana/oncall/pull/3345)) ## v1.3.57 (2023-11-10) diff --git a/engine/apps/alerts/tasks/acknowledge_reminder.py b/engine/apps/alerts/tasks/acknowledge_reminder.py index 1f8b6f1d..dd9848ac 100644 --- a/engine/apps/alerts/tasks/acknowledge_reminder.py +++ b/engine/apps/alerts/tasks/acknowledge_reminder.py @@ -26,13 +26,11 @@ def acknowledge_reminder_task(alert_group_pk: int, unacknowledge_process_id: str if unacknowledge_process_id != alert_group.last_unique_unacknowledge_process_id: return + organization = alert_group.channel.organization + # Get timeout values - acknowledge_reminder_timeout = Organization.ACKNOWLEDGE_REMIND_DELAY[ - alert_group.channel.organization.acknowledge_remind_timeout - ] - unacknowledge_timeout = Organization.UNACKNOWLEDGE_TIMEOUT_DELAY[ - alert_group.channel.organization.unacknowledge_timeout - ] + acknowledge_reminder_timeout = Organization.ACKNOWLEDGE_REMIND_DELAY[organization.acknowledge_remind_timeout] + unacknowledge_timeout = Organization.UNACKNOWLEDGE_TIMEOUT_DELAY[organization.unacknowledge_timeout] # Don't proceed if the alert group is not in a state for acknowledgement reminder acknowledge_reminder_required = ( @@ -41,10 +39,18 @@ def acknowledge_reminder_task(alert_group_pk: int, unacknowledge_process_id: str and alert_group.acknowledged_by == AlertGroup.USER and acknowledge_reminder_timeout ) - if not acknowledge_reminder_required: - task_logger.info("AlertGroup is not in a state for acknowledgement reminder") + is_organization_deleted = organization.deleted_at is not None + log_info = ( + f"acknowledge_reminder_timeout option: {acknowledge_reminder_timeout}," + f"organization ppk: {organization.public_primary_key}," + f"organization is deleted: {is_organization_deleted}" + ) + if not acknowledge_reminder_required or is_organization_deleted: + task_logger.info(f"alert group {alert_group_pk} is not in a state for acknowledgement reminder. {log_info}") return + task_logger.info(f"alert group {alert_group_pk} is in a state for acknowledgement reminder. {log_info}") + # unacknowledge_timeout_task uses acknowledged_by_confirmed to check if acknowledgement reminder has been confirmed # by the user. Setting to None here to indicate that the user has not confirmed the acknowledgement reminder alert_group.acknowledged_by_confirmed = None @@ -80,13 +86,11 @@ def unacknowledge_timeout_task(alert_group_pk: int, unacknowledge_process_id: st if unacknowledge_process_id != alert_group.last_unique_unacknowledge_process_id: return + organization = alert_group.channel.organization + # Get timeout values - acknowledge_reminder_timeout = Organization.ACKNOWLEDGE_REMIND_DELAY[ - alert_group.channel.organization.acknowledge_remind_timeout - ] - unacknowledge_timeout = Organization.UNACKNOWLEDGE_TIMEOUT_DELAY[ - alert_group.channel.organization.unacknowledge_timeout - ] + acknowledge_reminder_timeout = Organization.ACKNOWLEDGE_REMIND_DELAY[organization.acknowledge_remind_timeout] + unacknowledge_timeout = Organization.UNACKNOWLEDGE_TIMEOUT_DELAY[organization.unacknowledge_timeout] # Don't proceed if the alert group is not in a state for auto-unacknowledge unacknowledge_required = ( @@ -96,16 +100,28 @@ def unacknowledge_timeout_task(alert_group_pk: int, unacknowledge_process_id: st and acknowledge_reminder_timeout and unacknowledge_timeout ) - if not unacknowledge_required: - task_logger.info("AlertGroup is not in a state for unacknowledge") + is_organization_deleted = organization.deleted_at is not None + log_info = ( + f"acknowledge_reminder_timeout option: {acknowledge_reminder_timeout}," + f"unacknowledge_timeout option: {unacknowledge_timeout}," + f"organization ppk: {organization.public_primary_key}," + f"organization is deleted: {is_organization_deleted}" + ) + if not unacknowledge_required or is_organization_deleted: + task_logger.info(f"alert group {alert_group_pk} is not in a state for unacknowledge by timeout. {log_info}") return if alert_group.acknowledged_by_confirmed: # acknowledgement reminder was confirmed by the user acknowledge_reminder_task.apply_async( (alert_group_pk, unacknowledge_process_id), countdown=acknowledge_reminder_timeout - unacknowledge_timeout ) + task_logger.info( + f"Acknowledgement reminder was confirmed by user. Rescheduling acknowledge_reminder_task..." + f"alert group: {alert_group_pk}, {log_info}" + ) return + task_logger.info(f"alert group {alert_group_pk} is in a state for unacknowledge by timeout. {log_info}") # If acknowledgement reminder wasn't confirmed by the user, unacknowledge the alert group and start escalation again log_record = alert_group.log_records.create( type=AlertGroupLogRecord.TYPE_AUTO_UN_ACK, author=alert_group.acknowledged_by_user diff --git a/engine/apps/alerts/tests/test_acknowledge_reminder.py b/engine/apps/alerts/tests/test_acknowledge_reminder.py index a47042fc..f66fe9af 100644 --- a/engine/apps/alerts/tests/test_acknowledge_reminder.py +++ b/engine/apps/alerts/tests/test_acknowledge_reminder.py @@ -299,3 +299,43 @@ def test_unacknowledge_timeout_task_no_unacknowledge( ) assert not alert_group.log_records.exists() + + +@patch.object(acknowledge_reminder_task, "apply_async") +@patch.object(unacknowledge_timeout_task, "apply_async") +@pytest.mark.django_db +def test_ack_reminder_skip_deleted_org( + mock_acknowledge_reminder_task, + mock_unacknowledge_timeout_task, + ack_reminder_test_setup, +): + organization, alert_group, user = ack_reminder_test_setup() + organization.deleted_at = timezone.now() + organization.save() + + acknowledge_reminder_task(alert_group.pk, TASK_ID) + + mock_unacknowledge_timeout_task.assert_not_called() + mock_acknowledge_reminder_task.assert_not_called() + + assert not alert_group.log_records.exists() + + +@patch.object(acknowledge_reminder_task, "apply_async") +@patch.object(unacknowledge_timeout_task, "apply_async") +@pytest.mark.django_db +def test_unacknowledge_timeout_task_skip_deleted_org( + mock_acknowledge_reminder_task, + mock_unacknowledge_timeout_task, + ack_reminder_test_setup, +): + organization, alert_group, user = ack_reminder_test_setup() + organization.deleted_at = timezone.now() + organization.save() + + unacknowledge_timeout_task(alert_group.pk, TASK_ID) + + mock_unacknowledge_timeout_task.assert_not_called() + mock_acknowledge_reminder_task.assert_not_called() + + assert not alert_group.log_records.exists() From a9a2876b392d59c5743557962b8ae648bc388a31 Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Tue, 14 Nov 2023 15:28:05 +0100 Subject: [PATCH 13/15] use qrcode.react instead of react-qr-code lib (#3347) # What this PR does Use qrcode.react instead of react-qr-code library because the second one is buggy and doesn't set defaultProps correctly ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/3318 ## 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) --- CHANGELOG.md | 1 + grafana-plugin/package.json | 2 +- .../MobileAppConnection.test.tsx.snap | 2205 +---------------- .../parts/QRCode/QRCode.tsx | 4 +- .../QRCode/__snapshots__/QRCode.test.tsx.snap | 2205 +---------------- grafana-plugin/yarn.lock | 16 +- 6 files changed, 18 insertions(+), 4415 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e68637c1..1477da70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix styling when light theme is turned on via system preferences by excluding dark theme css vars in this case ([#3336](https://github.com/grafana/oncall/pull/3336)) - Fix issue when acknowledge reminder works for deleted organizations @Ferril ([#3345](https://github.com/grafana/oncall/pull/3345)) +- Fix generating QR code ([#3347](https://github.com/grafana/oncall/pull/3347)) ## v1.3.57 (2023-11-10) diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index a93cb29b..fb8278d3 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -128,13 +128,13 @@ "mobx-react": "6.1.1", "object-hash": "^3.0.0", "prettier": "^2.8.2", + "qrcode.react": "^3.1.0", "raw-loader": "^4.0.2", "rc-table": "^7.17.1", "react-copy-to-clipboard": "^5.0.2", "react-draggable": "^4.4.5", "react-emoji-render": "^1.2.4", "react-modal": "^3.15.1", - "react-qr-code": "^2.0.8", "react-responsive": "^8.1.0", "react-router-dom": "5.3.3", "react-sortable-hoc": "^1.11.0", diff --git a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap index 63dd5bef..74af36d7 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap +++ b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap @@ -134,2213 +134,18 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/grafana-plugin/src/containers/MobileAppConnection/parts/QRCode/QRCode.tsx b/grafana-plugin/src/containers/MobileAppConnection/parts/QRCode/QRCode.tsx index b41de79d..04380988 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/parts/QRCode/QRCode.tsx +++ b/grafana-plugin/src/containers/MobileAppConnection/parts/QRCode/QRCode.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; -import QRCodeBase from 'react-qr-code'; +import { QRCodeSVG } from 'qrcode.react'; import Block from 'components/GBlock/Block'; @@ -14,7 +14,7 @@ const QRCode: FC = (props: Props) => { return ( - + ); }; diff --git a/grafana-plugin/src/containers/MobileAppConnection/parts/QRCode/__snapshots__/QRCode.test.tsx.snap b/grafana-plugin/src/containers/MobileAppConnection/parts/QRCode/__snapshots__/QRCode.test.tsx.snap index 511496e9..8fa9a305 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/parts/QRCode/__snapshots__/QRCode.test.tsx.snap +++ b/grafana-plugin/src/containers/MobileAppConnection/parts/QRCode/__snapshots__/QRCode.test.tsx.snap @@ -7,2213 +7,18 @@ exports[`QRCode it renders properly 1`] = ` > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 77737039..f6b902f3 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -12475,10 +12475,10 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qr.js@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" - integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ== +qrcode.react@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8" + integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q== query-string@*: version "7.1.1" @@ -13224,14 +13224,6 @@ react-popper@2.3.0, react-popper@^2.3.0: react-fast-compare "^3.0.1" warning "^4.0.2" -react-qr-code@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.8.tgz#d34a766fb5b664a40dbdc7020f7ac801bacb2851" - integrity sha512-zYO9EAPQU8IIeD6c6uAle7NlKOiVKs8ji9hpbWPTGxO+FLqBN2on+XCXQvnhm91nrRd306RvNXUkUNcXXSfhWA== - dependencies: - prop-types "^15.8.1" - qr.js "0.0.0" - react-redux@^7.2.0: version "7.2.9" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" From 792cf61e789632109c882f0f1a4ba1507c179b5f Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 14 Nov 2023 15:36:38 +0000 Subject: [PATCH 14/15] Mention shift swaps in schedule quality docs (#3337) --- docs/sources/on-call-schedules/web-schedule/_index.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/sources/on-call-schedules/web-schedule/_index.md b/docs/sources/on-call-schedules/web-schedule/_index.md index e29d7cad..d77104ef 100644 --- a/docs/sources/on-call-schedules/web-schedule/_index.md +++ b/docs/sources/on-call-schedules/web-schedule/_index.md @@ -51,7 +51,7 @@ on this calendar will take precedence over the rotations calendar. ## Schedule quality report -The schedule view features a quality report that provides a score for your schedule based on rotations and overrides. +The schedule view features a quality report that provides a score for your schedule based on rotations, overrides and [shift swaps][shift-swaps]. It's calculated based on these key factors: - Gaps (amount of time when no one is on-call) @@ -85,3 +85,8 @@ A perfectly balanced schedule is considered ideal, so reducing this number will Export on-call schedules from Grafana OnCall to your preferred calendar app with a one-time secret iCal URL. The schedule export allows you to view on-call shifts alongside the rest of your schedule. + +{{% docs/reference %}} +[shift-swaps]: "/docs/oncall/ -> /docs/oncall//on-call-schedules/shift-swaps" +[shift-swaps]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/oncall/on-call-schedules/shift-swaps" +{{% /docs/reference %}} From 6d6a5c1123c01589722f90fffd2da8841bcaa5b2 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Tue, 14 Nov 2023 15:14:42 -0500 Subject: [PATCH 15/15] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1477da70..7fe15105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## v1.3.58 (2023-11-14) + ### Added - Added user timezone field to the users public API response ([#3311](https://github.com/grafana/oncall/pull/3311))