stabilize e2e tests (#3349)
# What this PR does Stabilize e2e tests by: - improve usage of locators - fix unreliable selectors - prevent parallelism within the same test file Additionally: - configure eslint for e2e tests and fix existing errors/warnings - bump Playwright version to latest stable ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/3217 ## 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)
This commit is contained in:
parent
719765a72d
commit
45ae04088f
25 changed files with 386 additions and 355 deletions
6
grafana-plugin/e2e-tests/.eslintrc
Normal file
6
grafana-plugin/e2e-tests/.eslintrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"rules": {
|
||||
"rulesdir/no-relative-import-paths": "off",
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ test('we can directly page a user', async ({ adminRolePage }) => {
|
|||
const { page } = adminRolePage;
|
||||
|
||||
await goToOnCallPage(page, 'alert-groups');
|
||||
await page.waitForTimeout(1000);
|
||||
await clickButton({ page, buttonText: 'Escalation' });
|
||||
|
||||
await fillInInput(page, 'textarea[name="message"]', message);
|
||||
await clickButton({ page, buttonText: 'Invite' });
|
||||
|
||||
|
|
@ -23,8 +23,14 @@ test('we can directly page a user', async ({ adminRolePage }) => {
|
|||
await addRespondersPopup.getByText('Users').click();
|
||||
await addRespondersPopup.getByText(adminRolePage.userName).click();
|
||||
|
||||
await clickButton({ page, buttonText: 'Create' });
|
||||
// If user is not on call, confirm invitation
|
||||
await page.waitForTimeout(1000);
|
||||
const isConfirmationModalShown = await page.getByText('Confirm Participant Invitation').isVisible();
|
||||
if (isConfirmationModalShown) {
|
||||
await page.getByTestId('confirm-non-oncall').click();
|
||||
}
|
||||
|
||||
await clickButton({ page, buttonText: 'Create' });
|
||||
// Check we are redirected to the alert group page
|
||||
await page.waitForURL('**/alert-groups/I*'); // Alert group IDs always start with "I"
|
||||
await expect(page.getByTestId('incident-message')).toContainText(message);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {expect, test} from "../fixtures";
|
||||
import {generateRandomValue} from "../utils/forms";
|
||||
import {createEscalationChain, EscalationStep, selectEscalationStepValue} from "../utils/escalationChain";
|
||||
import {generateRandomValue} from "../utils/forms";
|
||||
|
||||
test('escalation policy does not go back to "Default" after adding users to notify', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect, Page } from '../fixtures';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createEscalationChain } from '../utils/escalationChain';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
|
||||
const assertEscalationChainSearchWorks = async (
|
||||
page: Page,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test as base, Browser, Page, TestInfo } from '@playwright/test';
|
||||
|
||||
import { GRAFANA_ADMIN_USERNAME, GRAFANA_EDITOR_USERNAME, GRAFANA_VIEWER_USERNAME } from './utils/constants';
|
||||
import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config';
|
||||
|
||||
import { GRAFANA_ADMIN_USERNAME, GRAFANA_EDITOR_USERNAME, GRAFANA_VIEWER_USERNAME } from './utils/constants';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
|
||||
|
||||
export class BaseRolePage {
|
||||
page: Page;
|
||||
userName: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { OrgRole } from '@grafana/data';
|
||||
import { test as setup, chromium, expect, Page, BrowserContext, FullConfig, APIRequestContext } from '@playwright/test';
|
||||
|
||||
import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config';
|
||||
|
||||
import GrafanaAPIClient from './utils/clients/grafana';
|
||||
import {
|
||||
GRAFANA_ADMIN_PASSWORD,
|
||||
|
|
@ -14,8 +17,6 @@ import {
|
|||
} from './utils/constants';
|
||||
import { clickButton, getInputByName } from './utils/forms';
|
||||
import { goToGrafanaPage } from './utils/navigation';
|
||||
import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config';
|
||||
import { OrgRole } from '@grafana/data';
|
||||
|
||||
const grafanaApiClient = new GrafanaAPIClient(GRAFANA_ADMIN_USERNAME, GRAFANA_ADMIN_PASSWORD);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { test, Page, expect } from '../fixtures';
|
||||
|
||||
import { generateRandomValue, selectDropdownValue } from '../utils/forms';
|
||||
import { createIntegration } from '../utils/integrations';
|
||||
|
||||
|
|
@ -7,10 +6,7 @@ const HEARTBEAT_SETTINGS_FORM_TEST_ID = 'heartbeat-settings-form';
|
|||
|
||||
test.describe("updating an integration's heartbeat interval works", async () => {
|
||||
const _openHeartbeatSettingsForm = async (page: Page) => {
|
||||
const integrationSettingsPopupElement = page.getByTestId('integration-settings-context-menu');
|
||||
await integrationSettingsPopupElement.waitFor({ state: 'visible' });
|
||||
await integrationSettingsPopupElement.click();
|
||||
|
||||
await page.getByTestId('integration-settings-context-menu-wrapper').getByRole('img').click();
|
||||
await page.getByTestId('integration-heartbeat-settings').click();
|
||||
};
|
||||
|
||||
|
|
@ -60,6 +56,6 @@ test.describe("updating an integration's heartbeat interval works", async () =>
|
|||
*/
|
||||
await page.request.get(endpoint);
|
||||
await page.reload({ waitUntil: 'networkidle' });
|
||||
await page.getByTestId('heartbeat-badge').waitFor({ state: 'visible' });
|
||||
await page.getByTestId('heartbeat-badge').waitFor();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
import { test, expect } from '../fixtures';
|
||||
import { test } from '../fixtures';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createIntegration } from '../utils/integrations';
|
||||
import { createIntegration, searchIntegrationAndAssertItsPresence } 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`;
|
||||
const DIRECT_PAGING_INTEGRATION_NAME = `Direct paging integration name`;
|
||||
|
||||
// Create 2 integrations that are not Direct Paging
|
||||
await createIntegration({ page, integrationSearchText: 'Webhook', integrationName: WEBHOOK_INTEGRATION_NAME });
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByRole('tab', { name: 'Tab Integrations' }).click();
|
||||
|
||||
await createIntegration({
|
||||
page,
|
||||
integrationSearchText: 'Alertmanager',
|
||||
shouldGoToIntegrationsPage: false,
|
||||
integrationName: ALERTMANAGER_INTEGRATION_NAME,
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
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();
|
||||
const integrationsTable = page.getByTestId('integrations-table');
|
||||
await page.waitForTimeout(2000);
|
||||
const isDirectPagingAlreadyCreated = (await integrationsTable.getByText('Direct paging').count()) >= 1;
|
||||
if (!isDirectPagingAlreadyCreated) {
|
||||
await createIntegration({
|
||||
page,
|
||||
|
|
@ -31,17 +33,41 @@ test('Integrations table shows data in Connections and Direct Paging tabs', asyn
|
|||
shouldGoToIntegrationsPage: false,
|
||||
integrationName: DIRECT_PAGING_INTEGRATION_NAME,
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
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);
|
||||
await searchIntegrationAndAssertItsPresence({ page, integrationsTable, integrationName: WEBHOOK_INTEGRATION_NAME });
|
||||
await searchIntegrationAndAssertItsPresence({
|
||||
page,
|
||||
integrationsTable,
|
||||
integrationName: ALERTMANAGER_INTEGRATION_NAME,
|
||||
});
|
||||
await searchIntegrationAndAssertItsPresence({
|
||||
page,
|
||||
integrationsTable,
|
||||
integrationName: DIRECT_PAGING_INTEGRATION_NAME,
|
||||
visibleExpected: false,
|
||||
});
|
||||
|
||||
// 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);
|
||||
await searchIntegrationAndAssertItsPresence({
|
||||
page,
|
||||
integrationsTable,
|
||||
integrationName: WEBHOOK_INTEGRATION_NAME,
|
||||
visibleExpected: false,
|
||||
});
|
||||
await searchIntegrationAndAssertItsPresence({
|
||||
page,
|
||||
integrationsTable,
|
||||
integrationName: ALERTMANAGER_INTEGRATION_NAME,
|
||||
visibleExpected: false,
|
||||
});
|
||||
await searchIntegrationAndAssertItsPresence({
|
||||
page,
|
||||
integrationsTable,
|
||||
integrationName: 'Direct paging',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ import { goToOnCallPage } from '../utils/navigation';
|
|||
type MaintenanceModeType = 'Debug' | 'Maintenance';
|
||||
|
||||
test.describe('maintenance mode works', () => {
|
||||
test.slow(); // this test is doing a good amount of work, give it time
|
||||
|
||||
const MAINTENANCE_DURATION = '1 hour';
|
||||
const REMAINING_TIME_TEXT = '59m left';
|
||||
const REMAINING_TIME_TOOLTIP_TEST_ID = 'maintenance-mode-remaining-time-tooltip';
|
||||
|
|
@ -22,27 +20,27 @@ test.describe('maintenance mode works', () => {
|
|||
const createRoutedText = (escalationChainName: string): string =>
|
||||
`alert group assigned to route "default" with escalation chain "${escalationChainName}"`;
|
||||
|
||||
const _openIntegrationSettingsPopup = async (page: Page): Promise<Locator> => {
|
||||
const integrationSettingsPopupElement = page.getByTestId('integration-settings-context-menu');
|
||||
await integrationSettingsPopupElement.waitFor({ state: 'visible' });
|
||||
const _openIntegrationSettingsPopup = async (page: Page, shouldDoubleClickSettingsIcon = false): Promise<void> => {
|
||||
await page.waitForTimeout(2000);
|
||||
const integrationSettingsPopupElement = page
|
||||
.getByTestId('integration-settings-context-menu-wrapper')
|
||||
.getByRole('img');
|
||||
await integrationSettingsPopupElement.click();
|
||||
return integrationSettingsPopupElement;
|
||||
/**
|
||||
* sometimes we need to click twice (e.g. adding the escalation chain route
|
||||
* doesn't unfocus out of the select element after selecting an option)
|
||||
*/
|
||||
if (shouldDoubleClickSettingsIcon) {
|
||||
await integrationSettingsPopupElement.click();
|
||||
}
|
||||
};
|
||||
|
||||
const getRemainingTimeTooltip = (page: Page): Locator => page.getByTestId(REMAINING_TIME_TOOLTIP_TEST_ID);
|
||||
|
||||
const enableMaintenanceMode = async (page: Page, mode: MaintenanceModeType): Promise<void> => {
|
||||
const integrationSettingsPopupElement = await _openIntegrationSettingsPopup(page);
|
||||
/**
|
||||
* we need to click twice here, because adding the escalation chain route
|
||||
* doesn't unfocus out of the select element after selecting an option
|
||||
*/
|
||||
await integrationSettingsPopupElement.click();
|
||||
|
||||
await _openIntegrationSettingsPopup(page, true);
|
||||
// open the maintenance mode settings drawer + fill in the maintenance details
|
||||
const startMaintenanceModeButton = page.getByTestId('integration-start-maintenance');
|
||||
await startMaintenanceModeButton.waitFor({ state: 'visible' });
|
||||
await startMaintenanceModeButton.click();
|
||||
await page.getByTestId('integration-start-maintenance').click();
|
||||
|
||||
// fill in the form
|
||||
const maintenanceModeDrawer = page.getByTestId('maintenance-mode-drawer');
|
||||
|
|
@ -77,12 +75,10 @@ test.describe('maintenance mode works', () => {
|
|||
await goToOnCallPage(page, 'integrations');
|
||||
|
||||
await filterIntegrationsTableAndGoToDetailPage(page, integrationName);
|
||||
await _openIntegrationSettingsPopup(page);
|
||||
await _openIntegrationSettingsPopup(page, true);
|
||||
|
||||
// click the stop maintenance button
|
||||
const stopMaintenanceModeButton = page.getByTestId('integration-stop-maintenance');
|
||||
await stopMaintenanceModeButton.waitFor({ state: 'visible' });
|
||||
await stopMaintenanceModeButton.click();
|
||||
await page.getByTestId('integration-stop-maintenance').click();
|
||||
|
||||
// in the modal popup, confirm that we want to stop it
|
||||
await clickButton({
|
||||
|
|
@ -114,6 +110,8 @@ test.describe('maintenance mode works', () => {
|
|||
};
|
||||
|
||||
test('debug mode', async ({ adminRolePage: { page, userName } }) => {
|
||||
test.slow();
|
||||
|
||||
const { escalationChainName, integrationName } = await createIntegrationAndEscalationChainAndEnableMaintenanceMode(
|
||||
page,
|
||||
userName,
|
||||
|
|
@ -130,6 +128,7 @@ test.describe('maintenance mode works', () => {
|
|||
});
|
||||
|
||||
test('"maintenance" mode', async ({ adminRolePage: { page, userName } }) => {
|
||||
test.slow();
|
||||
const { integrationName } = await createIntegrationAndEscalationChainAndEnableMaintenanceMode(
|
||||
page,
|
||||
userName,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { test, expect } from '../fixtures';
|
||||
import { openCreateIntegrationModal } from '../utils/integrations';
|
||||
import { goToOnCallPage } from '../utils/navigation';
|
||||
|
||||
test('integrations have unique names', async ({ adminRolePage }) => {
|
||||
const { page } = adminRolePage;
|
||||
await goToOnCallPage(page, 'integrations');
|
||||
await openCreateIntegrationModal(page);
|
||||
|
||||
const integrationNames = await page.getByTestId('integration-display-name').allInnerTexts();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
import { test, expect } from '../fixtures';
|
||||
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||
import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
test('default dates in override creation modal are correct', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
|
|
|||
|
|
@ -1,136 +1,68 @@
|
|||
import { test, expect, Page } from '../fixtures';
|
||||
import { test, expect } from '../fixtures';
|
||||
import { goToOnCallPage } from '../utils/navigation';
|
||||
import { viewUsers, accessProfileTabs } from '../utils/users';
|
||||
|
||||
test.describe('Users screen actions', () => {
|
||||
test("Admin is allowed to edit other users' profile", async ({ adminRolePage }) => {
|
||||
await _testButtons(adminRolePage.page, 'button.edit-other-profile-button[disabled]');
|
||||
test("Admin is allowed to edit other users' profile", async ({ adminRolePage: { page } }) => {
|
||||
await goToOnCallPage(page, 'users');
|
||||
await expect(page.getByTestId('users-table').getByRole('button', { name: 'Edit', disabled: false })).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('Admin is allowed to view the list of users', async ({ adminRolePage }) => {
|
||||
await _viewUsers(adminRolePage.page);
|
||||
test('Admin is allowed to view the list of users', async ({ adminRolePage: { page } }) => {
|
||||
await viewUsers(page);
|
||||
});
|
||||
|
||||
test('Viewer is not allowed to view the list of users', async ({ viewerRolePage }) => {
|
||||
await _viewUsers(viewerRolePage.page, false);
|
||||
test('Viewer is not allowed to view the list of users', async ({ viewerRolePage: { page } }) => {
|
||||
await viewUsers(page, false);
|
||||
});
|
||||
|
||||
test('Viewer cannot access restricted tabs from View My Profile', async ({ viewerRolePage }) => {
|
||||
const { page } = viewerRolePage;
|
||||
|
||||
await _accessProfileTabs(page, ['tab-mobile-app', 'tab-phone-verification', 'tab-slack', 'tab-telegram'], false);
|
||||
await accessProfileTabs(page, ['tab-mobile-app', 'tab-phone-verification', 'tab-slack', 'tab-telegram'], false);
|
||||
});
|
||||
|
||||
test('Editor is allowed to view the list of users', async ({ editorRolePage }) => {
|
||||
await _viewUsers(editorRolePage.page);
|
||||
await viewUsers(editorRolePage.page);
|
||||
});
|
||||
|
||||
test("Editor cannot view other users' data", async ({ editorRolePage }) => {
|
||||
const { page } = editorRolePage;
|
||||
|
||||
await goToOnCallPage(page, 'users');
|
||||
await page.waitForSelector('.current-user');
|
||||
await page.getByTestId('users-email').and(page.getByText('editor')).waitFor();
|
||||
|
||||
// check if these fields are Masked or Not (******)
|
||||
const fieldIds = ['users-email', 'users-phone-number'];
|
||||
|
||||
for (let i = 0; i < fieldIds.length - 1; ++i) {
|
||||
const currentUsername = page.locator(`.current-user [data-testid="${fieldIds[i]}"]`);
|
||||
|
||||
expect((await currentUsername.all()).length).toBe(1); // match for current user
|
||||
(await currentUsername.all()).forEach((val) => expect(val).not.toHaveText('******'));
|
||||
|
||||
const otherUsername = page.locator(`.other-user [data-testid="${fieldIds[i]}"]`);
|
||||
|
||||
expect((await otherUsername.all()).length).toBeGreaterThan(1); // match for other users (>= 1)
|
||||
(await otherUsername.all()).forEach((val) => expect(val).toHaveText('******'));
|
||||
}
|
||||
await expect(page.getByTestId('users-email').and(page.getByText('editor'))).toHaveCount(1);
|
||||
await expect(page.getByTestId('users-email').and(page.getByText('******'))).toHaveCount(2);
|
||||
await expect(page.getByTestId('users-phone-number').and(page.getByText('******'))).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('Editor can access tabs from View My Profile', async ({ editorRolePage }) => {
|
||||
const { page } = editorRolePage;
|
||||
|
||||
// the other tabs depend on Cloud, skip for now
|
||||
await _accessProfileTabs(page, ['tab-slack', 'tab-telegram'], true);
|
||||
await accessProfileTabs(page, ['tab-slack', 'tab-telegram'], true);
|
||||
});
|
||||
|
||||
test("Editor is not allowed to edit other users' profile", async ({ editorRolePage }) => {
|
||||
await _testButtons(editorRolePage.page, 'button.edit-other-profile-button:not([disabled])');
|
||||
test("Editor is not allowed to edit other users' profile", async ({ editorRolePage: { page } }) => {
|
||||
await goToOnCallPage(page, 'users');
|
||||
await expect(page.getByTestId('users-table').getByRole('button', { name: 'Edit', disabled: false })).toHaveCount(1);
|
||||
await expect(page.getByTestId('users-table').getByRole('button', { name: 'Edit', disabled: true })).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('Search updates the table view', async ({ adminRolePage }) => {
|
||||
const { page } = adminRolePage;
|
||||
await goToOnCallPage(page, 'users');
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const searchInput = page.locator(`[data-testid="search-users"]`);
|
||||
|
||||
await searchInput.fill('oncall');
|
||||
await page.waitForTimeout(5000);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const result = page.locator(`[data-testid="users-username"]`);
|
||||
|
||||
expect(await result.count()).toBe(1);
|
||||
});
|
||||
|
||||
/*
|
||||
* Helper methods
|
||||
*/
|
||||
|
||||
async function _testButtons(page: Page, selector: string) {
|
||||
await goToOnCallPage(page, 'users');
|
||||
|
||||
const usersTableElement = page.getByTestId('users-table');
|
||||
await usersTableElement.waitFor({ state: 'visible' });
|
||||
|
||||
const buttonsList = await page.locator(selector);
|
||||
|
||||
expect(buttonsList).toHaveCount(0);
|
||||
}
|
||||
|
||||
async function _accessProfileTabs(page: Page, tabs: string[], hasAccess: boolean) {
|
||||
await goToOnCallPage(page, 'users');
|
||||
|
||||
await page.getByTestId('users-view-my-profile').click();
|
||||
|
||||
// the next queries could or could not resolve
|
||||
// therefore we wait a generic 1000ms duration and assert based on visibility
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
for (let i = 0; i < tabs.length - 1; ++i) {
|
||||
const tab = page.getByTestId(tabs[i]);
|
||||
|
||||
if (await tab.isVisible()) {
|
||||
await tab.click();
|
||||
|
||||
const query = page.getByText(
|
||||
'You do not have permission to perform this action. Ask an admin to upgrade your permissions.'
|
||||
);
|
||||
|
||||
if (hasAccess) {
|
||||
await expect(query).toBeHidden();
|
||||
} else {
|
||||
await expect(query).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function _viewUsers(page: Page, isAllowedToView = true): Promise<void> {
|
||||
await goToOnCallPage(page, 'users');
|
||||
|
||||
if (isAllowedToView) {
|
||||
const usersTableElement = page.getByTestId('users-table');
|
||||
await usersTableElement.waitFor({ state: 'visible' });
|
||||
|
||||
const userRowsContext = await usersTableElement.locator('tbody > tr').allTextContents();
|
||||
expect(userRowsContext.length).toBeGreaterThan(0);
|
||||
} else {
|
||||
const missingPermissionsMessageElement = page.getByTestId('view-users-missing-permission-message');
|
||||
await missingPermissionsMessageElement.waitFor({ state: 'visible' });
|
||||
|
||||
const missingPermissionMessage = await missingPermissionsMessageElement.textContent();
|
||||
expect(missingPermissionMessage).toMatch(/You are missing the .* to be able to view OnCall users/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Locator, Page, expect } from '@playwright/test';
|
||||
|
||||
import { selectDropdownValue, selectValuePickerValue } from './forms';
|
||||
import { goToOnCallPage } from './navigation';
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export default class GrafanaAPIClient {
|
|||
// user was just created
|
||||
const respJson: CreateUserResponse = await res.json();
|
||||
userId = respJson.id;
|
||||
} else if (responseCode == 412) {
|
||||
} else if (responseCode === 412) {
|
||||
// user already exists, go fetch their user id
|
||||
userId = await this.getUserIdByUsername(request, userName);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
type SelectorType = 'gSelect' | 'grafanaSelect';
|
||||
|
|
@ -22,9 +23,6 @@ type SelectDropdownValueArgs = {
|
|||
type ClickButtonArgs = {
|
||||
page: Page;
|
||||
buttonText: string;
|
||||
// if provided, search for the button by data-testid
|
||||
dataTestId?: string;
|
||||
|
||||
// if provided, use this Locator as the root of our search for the button
|
||||
startingLocator?: Locator;
|
||||
};
|
||||
|
|
@ -36,17 +34,9 @@ export const fillInInputByPlaceholderValue = (page: Page, placeholderValue: stri
|
|||
|
||||
export const getInputByName = (page: Page, name: string): Locator => page.locator(`input[name="${name}"]`);
|
||||
|
||||
export const clickButton = async ({
|
||||
page,
|
||||
buttonText,
|
||||
startingLocator,
|
||||
dataTestId,
|
||||
}: ClickButtonArgs): Promise<void> => {
|
||||
const baseLocator = dataTestId ? `button[data-testid="${dataTestId}"]` : 'button';
|
||||
const button = (startingLocator || page).locator(`${baseLocator}:not([disabled]) >> text=${buttonText}`);
|
||||
|
||||
await button.waitFor({ state: 'visible' });
|
||||
await button.click();
|
||||
export const clickButton = async ({ page, buttonText, startingLocator }: ClickButtonArgs): Promise<void> => {
|
||||
const baseLocator = startingLocator || page;
|
||||
await baseLocator.getByRole('button', { name: buttonText, disabled: false }).click();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -94,7 +84,7 @@ export const selectDropdownValue = async (args: SelectDropdownValueArgs): Promis
|
|||
const { page, value, pressEnterInsteadOfSelectingOption } = args;
|
||||
|
||||
const selectElement = await openSelect(args);
|
||||
await selectElement.type(value);
|
||||
await selectElement.pressSequentially(value);
|
||||
|
||||
if (pressEnterInsteadOfSelectingOption) {
|
||||
await page.keyboard.press('Enter');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Page } from '@playwright/test';
|
||||
import { Locator, Page, expect } from '@playwright/test';
|
||||
|
||||
import { clickButton, generateRandomValue, selectDropdownValue } from './forms';
|
||||
import { goToOnCallPage } from './navigation';
|
||||
|
||||
|
|
@ -60,8 +61,8 @@ export const assignEscalationChainToIntegration = async (page: Page, escalationC
|
|||
};
|
||||
|
||||
export const sendDemoAlert = async (page: Page): Promise<void> => {
|
||||
await clickButton({ page, buttonText: 'Send demo alert', dataTestId: 'send-demo-alert' });
|
||||
await clickButton({ page, buttonText: 'Send Alert', dataTestId: 'submit-send-alert' });
|
||||
await clickButton({ page, buttonText: 'Send demo alert' });
|
||||
await clickButton({ page, buttonText: 'Send Alert' });
|
||||
await page.getByTestId('demo-alert-sent-notification').waitFor({ state: 'visible' });
|
||||
};
|
||||
|
||||
|
|
@ -85,9 +86,32 @@ export const filterIntegrationsTableAndGoToDetailPage = async (page: Page, integ
|
|||
pressEnterInsteadOfSelectingOption: true,
|
||||
});
|
||||
|
||||
await (
|
||||
await page.waitForSelector(
|
||||
`div[data-testid="integrations-table"] table > tbody > tr > td:first-child a >> text=${integrationName}`
|
||||
)
|
||||
).click();
|
||||
await page.getByTestId('integrations-table').getByText(`${integrationName}`).click();
|
||||
};
|
||||
|
||||
export const searchIntegrationAndAssertItsPresence = async ({
|
||||
page,
|
||||
integrationName,
|
||||
integrationsTable,
|
||||
visibleExpected = true,
|
||||
}: {
|
||||
page: Page;
|
||||
integrationsTable: Locator;
|
||||
integrationName: string;
|
||||
visibleExpected?: boolean;
|
||||
}) => {
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Search or filter results\.\.\.$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.keyboard.insertText(integrationName);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(2000);
|
||||
const nbOfResults = await integrationsTable.getByText(integrationName).count();
|
||||
if (visibleExpected) {
|
||||
expect(nbOfResults).toBeGreaterThanOrEqual(1);
|
||||
} else {
|
||||
expect(nbOfResults).toBe(0);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Page, Response } from '@playwright/test';
|
||||
|
||||
import { BASE_URL } from './constants';
|
||||
|
||||
type GrafanaPage = '/plugins/grafana-oncall-app';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { Page } from '@playwright/test';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { clickButton, fillInInput, selectDropdownValue } from './forms';
|
||||
import { goToOnCallPage } from './navigation';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const createOnCallSchedule = async (page: Page, scheduleName: string, userName: string): Promise<void> => {
|
||||
// go to the schedules page
|
||||
|
|
|
|||
45
grafana-plugin/e2e-tests/utils/users.ts
Normal file
45
grafana-plugin/e2e-tests/utils/users.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
import { goToOnCallPage } from './navigation';
|
||||
|
||||
export async function accessProfileTabs(page: Page, tabs: string[], hasAccess: boolean) {
|
||||
await goToOnCallPage(page, 'users');
|
||||
|
||||
await page.getByTestId('users-view-my-profile').click();
|
||||
|
||||
// the next queries could or could not resolve
|
||||
// therefore we wait a generic 1000ms duration and assert based on visibility
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
for (let i = 0; i < tabs.length - 1; ++i) {
|
||||
const tab = page.getByTestId(tabs[i]);
|
||||
|
||||
if (await tab.isVisible()) {
|
||||
await tab.click();
|
||||
|
||||
const query = page.getByText(
|
||||
'You do not have permission to perform this action. Ask an admin to upgrade your permissions.'
|
||||
);
|
||||
|
||||
if (hasAccess) {
|
||||
await expect(query).toBeHidden();
|
||||
} else {
|
||||
await expect(query).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function viewUsers(page: Page, isAllowedToView = true): Promise<void> {
|
||||
await goToOnCallPage(page, 'users');
|
||||
|
||||
if (isAllowedToView) {
|
||||
const usersTable = page.getByTestId('users-table');
|
||||
await usersTable.getByRole('row').nth(1).waitFor();
|
||||
await expect(usersTable.getByRole('row')).toHaveCount(4);
|
||||
} else {
|
||||
await expect(page.getByTestId('view-users-missing-permission-message')).toHaveText(
|
||||
/You are missing the .* to be able to view OnCall users/
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
"version": "dev-oss",
|
||||
"description": "Grafana OnCall Plugin",
|
||||
"scripts": {
|
||||
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src",
|
||||
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 --quiet ./src",
|
||||
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src ./e2e-tests",
|
||||
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 --quiet ./src ./e2e-tests",
|
||||
"stylelint": "stylelint ./src/**/*.{css,scss,module.css,module.scss}",
|
||||
"stylelint:fix": "stylelint --fix ./src/**/*.{css,scss,module.css,module.scss}",
|
||||
"build": "grafana-toolkit plugin:build",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"@grafana/eslint-config": "^5.1.0",
|
||||
"@grafana/toolkit": "^9.5.2",
|
||||
"@jest/globals": "^27.5.1",
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "12",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
import { devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
|
|
@ -13,10 +11,12 @@ export const VIEWER_USER_STORAGE_STATE = path.join(__dirname, 'e2e-tests/.auth/v
|
|||
export const EDITOR_USER_STORAGE_STATE = path.join(__dirname, 'e2e-tests/.auth/editor.json');
|
||||
export const ADMIN_USER_STORAGE_STATE = path.join(__dirname, 'e2e-tests/.auth/admin.json');
|
||||
|
||||
const IS_CI = !!process.env.CI;
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
export default defineConfig({
|
||||
testDir: './e2e-tests',
|
||||
|
||||
/* Maximum time all the tests can run for. */
|
||||
|
|
@ -32,16 +32,16 @@ const config: PlaywrightTestConfig = {
|
|||
timeout: 10000,
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
fullyParallel: false,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
forbidOnly: IS_CI,
|
||||
/**
|
||||
* Retry on CI only
|
||||
*
|
||||
* NOTE: until we fix this issue (https://github.com/grafana/oncall/issues/1692) which occasionally leads
|
||||
* to flaky tests.. let's just retry failed tests. If the same test fails 3 times, you know something must be up
|
||||
* to flaky tests.. let's allow 1 retry per test
|
||||
*/
|
||||
retries: !!process.env.CI ? 3 : 0,
|
||||
retries: IS_CI ? 1 : 0,
|
||||
workers: 2,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
|
|
@ -52,10 +52,9 @@ const config: PlaywrightTestConfig = {
|
|||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on',
|
||||
video: 'on',
|
||||
headless: !!process.env.CI,
|
||||
headless: IS_CI,
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
|
|
@ -66,38 +65,28 @@ const config: PlaywrightTestConfig = {
|
|||
},
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
use: devices['Desktop Chrome'],
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
},
|
||||
use: devices['Desktop Firefox'],
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
},
|
||||
use: devices['Desktop Safari'],
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// use: devices['Pixel 5'],
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// use: devices['iPhone 12'],
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
|
|
@ -123,6 +112,4 @@ const config: PlaywrightTestConfig = {
|
|||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ const AddResponders = observer(
|
|||
<Button variant="secondary" onClick={closeUserConfirmationModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={confirmCurrentlyConsideredUser}>
|
||||
<Button variant="primary" onClick={confirmCurrentlyConsideredUser} data-testid="confirm-non-oncall">
|
||||
Confirm
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
|
|
|
|||
|
|
@ -834,164 +834,167 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
<WithContextMenu
|
||||
data-testid="integration-settings-context-menu"
|
||||
renderMenuItems={() => (
|
||||
<div className={cx('integration__actionsList')} id="integration-menu-options">
|
||||
<div className={cx('integration__actionItem')} onClick={() => openIntegrationSettings()}>
|
||||
<Text type="primary">Integration Settings</Text>
|
||||
</div>
|
||||
|
||||
{store.hasFeature(AppFeature.Labels) && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integration__actionItem')} onClick={() => openLabelsForm()}>
|
||||
<Text type="primary">Alert group labels</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
{showHeartbeatSettings() && (
|
||||
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={() => setIsHeartbeatFormOpen(true)}
|
||||
data-testid="integration-heartbeat-settings"
|
||||
>
|
||||
Heartbeat Settings
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
{!alertReceiveChannel.maintenance_till && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={openStartMaintenance}
|
||||
data-testid="integration-start-maintenance"
|
||||
>
|
||||
<Text type="primary">Start Maintenance</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
|
||||
<div className={cx('integration__actionItem')} onClick={changeIsTemplateSettingsOpen}>
|
||||
<Text type="primary">Edit Templates</Text>
|
||||
<div data-testid="integration-settings-context-menu-wrapper">
|
||||
<WithContextMenu
|
||||
renderMenuItems={() => (
|
||||
<div className={cx('integration__actionsList')} id="integration-menu-options">
|
||||
<div className={cx('integration__actionItem')} onClick={() => openIntegrationSettings()}>
|
||||
<Text type="primary">Integration Settings</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
{alertReceiveChannel.maintenance_till && (
|
||||
{store.hasFeature(AppFeature.Labels) && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integration__actionItem')} onClick={() => openLabelsForm()}>
|
||||
<Text type="primary">Alert group labels</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
{showHeartbeatSettings() && (
|
||||
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={() => setIsHeartbeatFormOpen(true)}
|
||||
data-testid="integration-heartbeat-settings"
|
||||
>
|
||||
Heartbeat Settings
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
{!alertReceiveChannel.maintenance_till && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={openStartMaintenance}
|
||||
data-testid="integration-start-maintenance"
|
||||
>
|
||||
<Text type="primary">Start Maintenance</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={() => {
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
confirmText: 'Stop',
|
||||
dismissText: 'Cancel',
|
||||
onConfirm: onStopMaintenance,
|
||||
title: 'Stop Maintenance',
|
||||
body: (
|
||||
<Text type="primary">
|
||||
Are you sure you want to stop the maintenance for{' '}
|
||||
<Emoji text={alertReceiveChannel.verbal_name} /> ?
|
||||
</Text>
|
||||
),
|
||||
});
|
||||
}}
|
||||
data-testid="integration-stop-maintenance"
|
||||
>
|
||||
<Text type="primary">Stop Maintenance</Text>
|
||||
<div className={cx('integration__actionItem')} onClick={changeIsTemplateSettingsOpen}>
|
||||
<Text type="primary">Edit Templates</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
{isLegacyIntegration && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={() =>
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: 'Migrate Integration?',
|
||||
body: (
|
||||
<VerticalGroup spacing="lg">
|
||||
{alertReceiveChannel.maintenance_till && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={() => {
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
confirmText: 'Stop',
|
||||
dismissText: 'Cancel',
|
||||
onConfirm: onStopMaintenance,
|
||||
title: 'Stop Maintenance',
|
||||
body: (
|
||||
<Text type="primary">
|
||||
Are you sure you want to migrate <Emoji text={alertReceiveChannel.verbal_name} /> ?
|
||||
Are you sure you want to stop the maintenance for{' '}
|
||||
<Emoji text={alertReceiveChannel.verbal_name} /> ?
|
||||
</Text>
|
||||
),
|
||||
});
|
||||
}}
|
||||
data-testid="integration-stop-maintenance"
|
||||
>
|
||||
<Text type="primary">Stop Maintenance</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
<VerticalGroup spacing="xs">
|
||||
<Text type="secondary">- Integration internal behaviour will be changed</Text>
|
||||
<Text type="secondary">
|
||||
- Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '}
|
||||
configuration
|
||||
{isLegacyIntegration && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={() =>
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: 'Migrate Integration?',
|
||||
body: (
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text type="primary">
|
||||
Are you sure you want to migrate <Emoji text={alertReceiveChannel.verbal_name} /> ?
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
- Integration templates will be reset to suit the new payload
|
||||
</Text>
|
||||
<Text type="secondary">- It is needed to adjust routes manually to the new payload</Text>
|
||||
|
||||
<VerticalGroup spacing="xs">
|
||||
<Text type="secondary">- Integration internal behaviour will be changed</Text>
|
||||
<Text type="secondary">
|
||||
- Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '}
|
||||
configuration
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
- Integration templates will be reset to suit the new payload
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
- It is needed to adjust routes manually to the new payload
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</VerticalGroup>
|
||||
</VerticalGroup>
|
||||
),
|
||||
onConfirm: onIntegrationMigrate,
|
||||
dismissText: 'Cancel',
|
||||
confirmText: 'Migrate',
|
||||
})
|
||||
}
|
||||
>
|
||||
Migrate
|
||||
),
|
||||
onConfirm: onIntegrationMigrate,
|
||||
dismissText: 'Cancel',
|
||||
confirmText: 'Migrate',
|
||||
})
|
||||
}
|
||||
>
|
||||
Migrate
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
<CopyToClipboard
|
||||
text={alertReceiveChannel.id}
|
||||
onCopy={() => openNotification('Integration ID is copied')}
|
||||
>
|
||||
<div className={cx('integration__actionItem')}>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="copy" />
|
||||
|
||||
<Text type="primary">UID: {alertReceiveChannel.id}</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
|
||||
<div className={cx('thin-line-break')} />
|
||||
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integration__actionItem')}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: 'Delete Integration?',
|
||||
body: (
|
||||
<Text type="primary">
|
||||
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} /> ?
|
||||
</Text>
|
||||
),
|
||||
onConfirm: deleteIntegration,
|
||||
dismissText: 'Cancel',
|
||||
confirmText: 'Delete',
|
||||
});
|
||||
}}
|
||||
className="u-width-100"
|
||||
>
|
||||
<Text type="danger">
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="trash-alt" />
|
||||
<span>Delete Integration</span>
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
<CopyToClipboard
|
||||
text={alertReceiveChannel.id}
|
||||
onCopy={() => openNotification('Integration ID is copied')}
|
||||
>
|
||||
<div className={cx('integration__actionItem')}>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="copy" />
|
||||
|
||||
<Text type="primary">UID: {alertReceiveChannel.id}</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
|
||||
<div className={cx('thin-line-break')} />
|
||||
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integration__actionItem')}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: 'Delete Integration?',
|
||||
body: (
|
||||
<Text type="primary">
|
||||
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} /> ?
|
||||
</Text>
|
||||
),
|
||||
onConfirm: deleteIntegration,
|
||||
dismissText: 'Cancel',
|
||||
confirmText: 'Delete',
|
||||
});
|
||||
}}
|
||||
className="u-width-100"
|
||||
>
|
||||
<Text type="danger">
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="trash-alt" />
|
||||
<span>Delete Integration</span>
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <HamburgerMenu openMenu={openMenu} listBorder={2} listWidth={200} withBackground />}
|
||||
</WithContextMenu>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <HamburgerMenu openMenu={openMenu} listBorder={2} listWidth={200} withBackground />}
|
||||
</WithContextMenu>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
{
|
||||
"type": "page",
|
||||
"name": "Integrations",
|
||||
"path": "/a/grafana-oncall-app/integrations",
|
||||
"path": "/a/grafana-oncall-app/integrations?tab=connections",
|
||||
"role": "Viewer",
|
||||
"action": "grafana-oncall-app.integrations:read",
|
||||
"addToNav": true
|
||||
|
|
|
|||
|
|
@ -3193,15 +3193,12 @@
|
|||
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.6.6.tgz#641f73913a6be402b34e4bdfca98d6832ed55586"
|
||||
integrity sha512-3MUulwMtsdCA9lw8a/Kc0XDBJJVCkYTQ5aGd+///TbfkOMXoOGAzzoiYKwPEsLYZv7He7fKJ/mCacqKOO7REyg==
|
||||
|
||||
"@playwright/test@^1.35.1":
|
||||
version "1.35.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.35.1.tgz#a596b61e15b980716696f149cc7a2002f003580c"
|
||||
integrity sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==
|
||||
"@playwright/test@^1.39.0":
|
||||
version "1.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.39.0.tgz#d10ba8e38e44104499e25001945f07faa9fa91cd"
|
||||
integrity sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
playwright-core "1.35.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
playwright "1.39.0"
|
||||
|
||||
"@polka/url@^1.0.0-next.20":
|
||||
version "1.0.0-next.21"
|
||||
|
|
@ -11757,10 +11754,19 @@ pkg-up@^3.1.0:
|
|||
dependencies:
|
||||
find-up "^3.0.0"
|
||||
|
||||
playwright-core@1.35.1:
|
||||
version "1.35.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.1.tgz#52c1e6ffaa6a8c29de1a5bdf8cce0ce290ffb81d"
|
||||
integrity sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==
|
||||
playwright-core@1.39.0:
|
||||
version "1.39.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.39.0.tgz#efeaea754af4fb170d11845b8da30b2323287c63"
|
||||
integrity sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==
|
||||
|
||||
playwright@1.39.0:
|
||||
version "1.39.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.39.0.tgz#184c81cd6478f8da28bcd9e60e94fcebf566e077"
|
||||
integrity sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==
|
||||
dependencies:
|
||||
playwright-core "1.39.0"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
please-upgrade-node@^3.2.0:
|
||||
version "3.2.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue