add a couple of tests for users screen (#2612)
# What this PR does There are the following tests added: - admin is allowed to edit other profiles - editor is not allowed to edit other profiles ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/1586 ## Checklist - [x] 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) --------- Co-authored-by: Rares Mardare <rares.mardare@grafana.com>
This commit is contained in:
parent
8eacbf2500
commit
36f9851003
41 changed files with 396 additions and 285 deletions
2
.github/workflows/linting-and-tests.yml
vendored
2
.github/workflows/linting-and-tests.yml
vendored
|
|
@ -479,7 +479,7 @@ jobs:
|
|||
GRAFANA_VIEWER_PASSWORD: viewer
|
||||
MAILSLURP_API_KEY: ${{ secrets.MAILSLURP_API_KEY }}
|
||||
working-directory: ./grafana-plugin
|
||||
run: yarn test:integration
|
||||
run: yarn test:e2e
|
||||
|
||||
# spit out the engine, celery, and grafana logs, if the the e2e tests have failed
|
||||
# can be helpful for debugging failing tests
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
- [Enabling RBAC for OnCall for local development](#enabling-rbac-for-oncall-for-local-development)
|
||||
- [Django Silk Profiling](#django-silk-profiling)
|
||||
- [Running backend services outside Docker](#running-backend-services-outside-docker)
|
||||
- [UI Integration Tests](#ui-integration-tests)
|
||||
- [UI E2E Tests](#ui-e2e-tests)
|
||||
- [Useful `make` commands](#useful-make-commands)
|
||||
- [Setting environment variables](#setting-environment-variables)
|
||||
- [Slack application setup](#slack-application-setup)
|
||||
|
|
@ -189,7 +189,7 @@ By default everything runs inside Docker. If you would like to run the backend s
|
|||
- `make run-backend-server` - runs the HTTP server
|
||||
- `make run-backend-celery` - runs Celery workers
|
||||
|
||||
## UI Integration Tests
|
||||
## UI E2E Tests
|
||||
|
||||
We've developed a suite of "end-to-end" integration tests using [Playwright](https://playwright.dev/). These tests
|
||||
are run on pull request CI builds. New features should ideally include a new/modified integration test.
|
||||
|
|
@ -198,10 +198,10 @@ To run these tests locally simply do the following:
|
|||
|
||||
```bash
|
||||
npx playwright install # install playwright dependencies
|
||||
cp ./grafana-plugin/integration-tests/.env.example ./grafana-plugin/integration-tests/.env
|
||||
cp ./grafana-plugin/e2e-tests/.env.example ./grafana-plugin/e2e-tests/.env
|
||||
# you may need to tweak the values in ./grafana-plugin/.env according to your local setup
|
||||
cd grafana-plugin
|
||||
yarn test:integration
|
||||
yarn test:e2e
|
||||
```
|
||||
|
||||
## Useful `make` commands
|
||||
|
|
|
|||
2
grafana-plugin/.gitignore
vendored
2
grafana-plugin/.gitignore
vendored
|
|
@ -19,4 +19,4 @@ frontend_enterprise
|
|||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
/integration-tests/storageState.json
|
||||
/e2e-tests/storageState.json
|
||||
|
|
|
|||
120
grafana-plugin/e2e-tests/users/usersActions.test.ts
Normal file
120
grafana-plugin/e2e-tests/users/usersActions.test.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { test, expect, Page } from '../fixtures';
|
||||
import { goToOnCallPage } from '../utils/navigation';
|
||||
|
||||
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 view the list of users', async ({ adminRolePage }) => {
|
||||
await _viewUsers(adminRolePage.page);
|
||||
});
|
||||
|
||||
test('Viewer is not allowed to view the list of users', async ({ viewerRolePage }) => {
|
||||
await _viewUsers(viewerRolePage.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);
|
||||
});
|
||||
|
||||
test('Editor is allowed to view the list of users', async ({ editorRolePage }) => {
|
||||
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');
|
||||
|
||||
// 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('******'));
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
test("Editor is not allowed to edit other users' profile", async ({ editorRolePage }) => {
|
||||
await _testButtons(editorRolePage.page, 'button.edit-other-profile-button:not([disabled])');
|
||||
});
|
||||
|
||||
/*
|
||||
* 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,34 +0,0 @@
|
|||
import { test, expect, Page } from '../fixtures';
|
||||
import { goToOnCallPage } from '../utils/navigation';
|
||||
|
||||
test.describe('view list of users', () => {
|
||||
const testFlow = async (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/);
|
||||
}
|
||||
};
|
||||
|
||||
test('admin is allowed to', async ({ adminRolePage }) => {
|
||||
await testFlow(adminRolePage.page);
|
||||
});
|
||||
|
||||
test('editor is allowed to', async ({ editorRolePage }) => {
|
||||
await testFlow(editorRolePage.page);
|
||||
});
|
||||
|
||||
test('viewer is not allowed to', async ({ viewerRolePage }) => {
|
||||
await testFlow(viewerRolePage.page, false);
|
||||
});
|
||||
});
|
||||
|
|
@ -21,5 +21,5 @@ module.exports = {
|
|||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
|
||||
testTimeout: 10000,
|
||||
testPathIgnorePatterns: ['/node_modules/', '/integration-tests/'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/e2e-tests/'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
"build:dev": "grafana-toolkit plugin:build --skipTest --skipLint",
|
||||
"test": "jest --verbose",
|
||||
"test:silent": "jest --silent",
|
||||
"test:integration": "yarn playwright test",
|
||||
"test:e2e": "yarn playwright test",
|
||||
"dev": "grafana-toolkit plugin:dev",
|
||||
"watch": "grafana-toolkit plugin:dev --watch",
|
||||
"sign": "grafana-toolkit plugin:sign",
|
||||
|
|
|
|||
|
|
@ -7,17 +7,17 @@ import { devices } from '@playwright/test';
|
|||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
require('dotenv').config({ path: path.resolve(process.cwd(), 'integration-tests/.env') });
|
||||
require('dotenv').config({ path: path.resolve(process.cwd(), 'e2e-tests/.env') });
|
||||
|
||||
export const VIEWER_USER_STORAGE_STATE = path.join(__dirname, 'integration-tests/.auth/viewer.json');
|
||||
export const EDITOR_USER_STORAGE_STATE = path.join(__dirname, 'integration-tests/.auth/editor.json');
|
||||
export const ADMIN_USER_STORAGE_STATE = path.join(__dirname, 'integration-tests/.auth/admin.json');
|
||||
export const VIEWER_USER_STORAGE_STATE = path.join(__dirname, 'e2e-tests/.auth/viewer.json');
|
||||
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');
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './integration-tests',
|
||||
testDir: './e2e-tests',
|
||||
|
||||
/* Maximum time all the tests can run for. */
|
||||
globalTimeout: 20 * 60 * 1000, // 20 minutes
|
||||
|
|
|
|||
|
|
@ -40,10 +40,7 @@ const MobileAppConnection = observer(({ userPk }: Props) => {
|
|||
// Show link to cloud page for OSS instances with no cloud connection
|
||||
if (store.hasFeature(AppFeature.CloudConnection) && !cloudStore.cloudConnectionStatus.cloud_connection_status) {
|
||||
return (
|
||||
<WithPermissionControlDisplay
|
||||
userAction={UserActions.UserSettingsWrite}
|
||||
message="You do not have permission to perform this action. Ask an admin to upgrade your permissions."
|
||||
>
|
||||
<WithPermissionControlDisplay userAction={UserActions.UserSettingsWrite}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text type="secondary">Please connect Cloud OnCall to use the mobile app</Text>
|
||||
<WithPermissionControlDisplay
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import Block from 'components/GBlock/Block';
|
|||
import MobileAppConnection from 'containers/MobileAppConnection/MobileAppConnection';
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
import { SlackTab } from 'containers/UserSettings/parts/tabs//SlackTab/SlackTab';
|
||||
import CloudPhoneSettings from 'containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings';
|
||||
import { NotificationSettingsTab } from 'containers/UserSettings/parts/tabs/NotificationSettingsTab';
|
||||
import PhoneVerification from 'containers/UserSettings/parts/tabs/PhoneVerification/PhoneVerification';
|
||||
import TelegramInfo from 'containers/UserSettings/parts/tabs/TelegramInfo/TelegramInfo';
|
||||
|
|
@ -17,6 +16,8 @@ import { User } from 'models/user/user.types';
|
|||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import CloudPhoneSettings from './tabs/CloudPhoneSettings/CloudPhoneSettings';
|
||||
|
||||
import styles from 'containers/UserSettings/parts/index.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -54,6 +55,7 @@ export const Tabs = ({
|
|||
label="User Info"
|
||||
key={UserSettingsTab.UserInfo}
|
||||
onChangeTab={getTabClickHandler(UserSettingsTab.UserInfo)}
|
||||
data-testid="tab-user-info"
|
||||
/>
|
||||
{showNotificationSettingsTab && (
|
||||
<Tab
|
||||
|
|
@ -61,6 +63,7 @@ export const Tabs = ({
|
|||
label="Notification Settings"
|
||||
key={UserSettingsTab.NotificationSettings}
|
||||
onChangeTab={getTabClickHandler(UserSettingsTab.NotificationSettings)}
|
||||
data-testid="tab-notification-settings"
|
||||
/>
|
||||
)}
|
||||
<Tab
|
||||
|
|
@ -68,6 +71,7 @@ export const Tabs = ({
|
|||
label="Phone Verification"
|
||||
key={UserSettingsTab.PhoneVerification}
|
||||
onChangeTab={getTabClickHandler(UserSettingsTab.PhoneVerification)}
|
||||
data-testid="tab-phone-verification"
|
||||
/>
|
||||
{showMobileAppConnectionTab && (
|
||||
<Tab
|
||||
|
|
@ -75,6 +79,7 @@ export const Tabs = ({
|
|||
label="Mobile App Connection"
|
||||
key={UserSettingsTab.MobileAppConnection}
|
||||
onChangeTab={getTabClickHandler(UserSettingsTab.MobileAppConnection)}
|
||||
data-testid="tab-mobile-app"
|
||||
/>
|
||||
)}
|
||||
{showSlackConnectionTab && (
|
||||
|
|
@ -83,6 +88,7 @@ export const Tabs = ({
|
|||
label="Slack Connection"
|
||||
key={UserSettingsTab.SlackInfo}
|
||||
onChangeTab={getTabClickHandler(UserSettingsTab.SlackInfo)}
|
||||
data-testid="tab-slack"
|
||||
/>
|
||||
)}
|
||||
{showTelegramConnectionTab && (
|
||||
|
|
@ -91,6 +97,7 @@ export const Tabs = ({
|
|||
label="Telegram Connection"
|
||||
key={UserSettingsTab.TelegramInfo}
|
||||
onChangeTab={getTabClickHandler(UserSettingsTab.TelegramInfo)}
|
||||
data-testid="tab-telegram"
|
||||
/>
|
||||
)}
|
||||
</TabsBar>
|
||||
|
|
|
|||
|
|
@ -111,27 +111,24 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<WithPermissionControlDisplay
|
||||
userAction={UserActions.OtherSettingsWrite}
|
||||
title="OnCall uses Grafana Cloud for SMS and phone call notifications"
|
||||
message="You do not have permission to perform this action. Ask an admin to upgrade your permissions."
|
||||
>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text.Title level={3}>OnCall uses Grafana Cloud for SMS and phone call notifications</Text.Title>
|
||||
{syncing ? (
|
||||
<Button icon="sync" variant="secondary" disabled>
|
||||
Updating...
|
||||
</Button>
|
||||
) : (
|
||||
<Button icon="sync" variant="secondary" onClick={syncUser} disabled={userStatus === 0}>
|
||||
Reload from Cloud
|
||||
</Button>
|
||||
)}
|
||||
{!syncing ? <UserCloudStatus /> : <LoadingPlaceholder text="Loading..." />}
|
||||
</VerticalGroup>
|
||||
</WithPermissionControlDisplay>
|
||||
</>
|
||||
<WithPermissionControlDisplay
|
||||
userAction={UserActions.OtherSettingsWrite}
|
||||
title="OnCall uses Grafana Cloud for SMS and phone call notifications"
|
||||
>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text.Title level={3}>OnCall uses Grafana Cloud for SMS and phone call notifications</Text.Title>
|
||||
{syncing ? (
|
||||
<Button icon="sync" variant="secondary" disabled>
|
||||
Updating...
|
||||
</Button>
|
||||
) : (
|
||||
<Button icon="sync" variant="secondary" onClick={syncUser} disabled={userStatus === 0}>
|
||||
Reload from Cloud
|
||||
</Button>
|
||||
)}
|
||||
{!syncing ? <UserCloudStatus /> : <LoadingPlaceholder text="Loading..." />}
|
||||
</VerticalGroup>
|
||||
</WithPermissionControlDisplay>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { observer } from 'mobx-react';
|
|||
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import { WithPermissionControlDisplay } from 'containers/WithPermissionControl/WithPermissionControlDisplay';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { rootStore } from 'state';
|
||||
|
|
@ -187,31 +188,28 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPhoneValid && !user.verified_phone_number && (
|
||||
<>
|
||||
<Alert severity="info" title="You will receive alerts to a new number after verification" />
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isPhoneProviderConfigured && store.hasFeature(AppFeature.LiveSettings) && (
|
||||
<>
|
||||
<Alert
|
||||
severity="warning"
|
||||
// @ts-ignore
|
||||
title={
|
||||
<>
|
||||
Can't verify phone. <PluginLink query={{ page: 'live-settings' }}> Check ENV variables</PluginLink> to
|
||||
configure your provider.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
|
||||
<WithPermissionControlDisplay userAction={UserActions.OtherSettingsWrite}>
|
||||
<VerticalGroup>
|
||||
{isPhoneValid && !user.verified_phone_number && (
|
||||
<Alert severity="info" title="You will receive alerts to a new number after verification" />
|
||||
)}
|
||||
|
||||
{!isPhoneProviderConfigured && store.hasFeature(AppFeature.LiveSettings) && (
|
||||
<>
|
||||
<Alert
|
||||
severity="warning"
|
||||
title={
|
||||
(
|
||||
<Text type="primary">
|
||||
Can't verify phone. <PluginLink query={{ page: 'live-settings' }}> Check ENV variables</PluginLink>{' '}
|
||||
to configure your provider.
|
||||
</Text>
|
||||
) as any
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Field
|
||||
className={cx('phone__field')}
|
||||
invalid={showPhoneInputError}
|
||||
|
|
@ -224,8 +222,7 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
|
|||
required
|
||||
disabled={!isPhoneProviderConfigured || isPhoneDisabled}
|
||||
placeholder="Please enter the phone number with country code, e.g. +12451111111"
|
||||
// @ts-ignore
|
||||
prefix={<Icon name="phone" />}
|
||||
prefix={<Icon name={'phone' as any} />}
|
||||
value={phone}
|
||||
onChange={onChangePhoneCallback}
|
||||
/>
|
||||
|
|
@ -263,25 +260,23 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
|
|||
<label className={cx('switch__label')}>Hide my phone number from teammates</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PhoneVerificationButtonsGroup
|
||||
action={action}
|
||||
isCodeSent={isCodeSent}
|
||||
isPhoneCallInitiated={isPhoneCallInitiated}
|
||||
isButtonDisabled={isButtonDisabled}
|
||||
isTestCallInProgress={userStore.isTestCallInProgress}
|
||||
providerConfiguration={providerConfiguration}
|
||||
onSubmitCallback={onSubmitCallback}
|
||||
onVerifyCallback={onVerifyCallback}
|
||||
handleMakeTestCallClick={handleMakeTestCallClick}
|
||||
handleSendTestSmsClick={handleSendTestSmsClick}
|
||||
onShowForgetScreen={() => setState({ showForgetScreen: true })}
|
||||
user={user}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<PhoneVerificationButtonsGroup
|
||||
action={action}
|
||||
isCodeSent={isCodeSent}
|
||||
isPhoneCallInitiated={isPhoneCallInitiated}
|
||||
isButtonDisabled={isButtonDisabled}
|
||||
isTestCallInProgress={userStore.isTestCallInProgress}
|
||||
providerConfiguration={providerConfiguration}
|
||||
onSubmitCallback={onSubmitCallback}
|
||||
onVerifyCallback={onVerifyCallback}
|
||||
handleMakeTestCallClick={handleMakeTestCallClick}
|
||||
handleSendTestSmsClick={handleSendTestSmsClick}
|
||||
onShowForgetScreen={() => setState({ showForgetScreen: true })}
|
||||
user={user}
|
||||
/>
|
||||
</>
|
||||
</WithPermissionControlDisplay>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -363,7 +358,6 @@ function PhoneVerificationButtonsGroup({
|
|||
</>
|
||||
) : (
|
||||
<HorizontalGroup>
|
||||
{' '}
|
||||
{providerConfiguration.verification_sms && (
|
||||
<WithPermissionControlTooltip userAction={action}>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import cn from 'classnames/bind';
|
|||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import Text from 'components/Text/Text';
|
||||
import { WithPermissionControlDisplay } from 'containers/WithPermissionControl/WithPermissionControlDisplay';
|
||||
import { SlackNewIcon } from 'icons';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { DOCS_SLACK_SETUP } from 'utils/consts';
|
||||
|
||||
import styles from './SlackTab.module.css';
|
||||
|
|
@ -21,32 +23,34 @@ export const SlackTab = () => {
|
|||
}, [slackStore]);
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="lg">
|
||||
<Block bordered withBackground className={cx('slack-infoblock', 'personal-slack-infoblock')}>
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<SlackNewIcon />
|
||||
<Text>
|
||||
Personal Slack connection will allow you to manage alert groups in your connected team's Internal Slack
|
||||
workspace.
|
||||
</Text>
|
||||
<Text>To setup personal Slack click the button below, choose workspace and click Allow.</Text>
|
||||
<WithPermissionControlDisplay userAction={UserActions.UserSettingsWrite}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Block bordered withBackground className={cx('slack-infoblock', 'personal-slack-infoblock')}>
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<SlackNewIcon />
|
||||
<Text>
|
||||
Personal Slack connection will allow you to manage alert groups in your connected team's Internal Slack
|
||||
workspace.
|
||||
</Text>
|
||||
<Text>To setup personal Slack click the button below, choose workspace and click Allow.</Text>
|
||||
|
||||
<Text type="secondary">
|
||||
More details in{' '}
|
||||
<a href={DOCS_SLACK_SETUP} target="_blank" rel="noreferrer">
|
||||
<Text type="link">our documentation</Text>
|
||||
</a>
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
More details in{' '}
|
||||
<a href={DOCS_SLACK_SETUP} target="_blank" rel="noreferrer">
|
||||
<Text type="link">our documentation</Text>
|
||||
</a>
|
||||
</Text>
|
||||
|
||||
<img
|
||||
style={{ height: '350px', display: 'block', margin: '0 auto' }}
|
||||
src="public/plugins/grafana-oncall-app/assets/img/slack_instructions.png"
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
<Button onClick={handleClickConnectSlackAccount}>
|
||||
<Icon name="external-link-alt" className={cx('external-link-style')} /> Open Slack connection page
|
||||
</Button>
|
||||
</VerticalGroup>
|
||||
<img
|
||||
style={{ height: '350px', display: 'block', margin: '0 auto' }}
|
||||
src="public/plugins/grafana-oncall-app/assets/img/slack_instructions.png"
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
<Button onClick={handleClickConnectSlackAccount}>
|
||||
<Icon name="external-link-alt" className={cx('external-link-style')} /> Open Slack connection page
|
||||
</Button>
|
||||
</VerticalGroup>
|
||||
</WithPermissionControlDisplay>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,10 +39,7 @@ const TelegramInfo = observer((_props: TelegramInfoProps) => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<WithPermissionControlDisplay
|
||||
userAction={UserActions.UserSettingsWrite}
|
||||
message="You do not have permission to perform this action. Ask an admin to upgrade your permissions."
|
||||
>
|
||||
<WithPermissionControlDisplay userAction={UserActions.UserSettingsWrite}>
|
||||
{telegramConfigured || !store.hasFeature(AppFeature.LiveSettings) ? (
|
||||
<VerticalGroup>
|
||||
<Text.Title level={5}>Manual connection</Text.Title>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,17 @@ import { isUserActionAllowed, UserAction } from 'utils/authorization';
|
|||
interface WithPermissionControlDisplayProps {
|
||||
userAction: UserAction;
|
||||
children: ReactElement;
|
||||
message: string;
|
||||
message?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const WithPermissionControlDisplay: React.FC<WithPermissionControlDisplayProps> = (props) => {
|
||||
const { userAction, children, title, message } = props;
|
||||
const {
|
||||
userAction,
|
||||
children,
|
||||
title,
|
||||
message = 'You do not have permission to perform this action. Ask an admin to upgrade your permissions.',
|
||||
} = props;
|
||||
|
||||
const hasPermission = isUserActionAllowed(userAction);
|
||||
|
||||
|
|
|
|||
|
|
@ -121,55 +121,14 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { usersFilters, userPkToEdit, page, errorData, initialUsersLoaded } = this.state;
|
||||
const { userPkToEdit, errorData } = this.state;
|
||||
const {
|
||||
store,
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
} = this.props;
|
||||
const { userStore } = store;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
width: '25%',
|
||||
key: 'username',
|
||||
title: 'User',
|
||||
render: this.renderTitle,
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
title: 'Status',
|
||||
key: 'note',
|
||||
render: this.renderStatus,
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
title: 'Default Notifications',
|
||||
key: 'notifications-chain',
|
||||
render: this.renderNotificationsChain,
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
title: 'Important Notifications',
|
||||
key: 'important-notifications-chain',
|
||||
render: this.renderImportantNotificationsChain,
|
||||
},
|
||||
{
|
||||
width: '5%',
|
||||
key: 'buttons',
|
||||
render: this.renderButtons,
|
||||
},
|
||||
];
|
||||
|
||||
const handleClear = () =>
|
||||
this.setState({ usersFilters: { searchTerm: '' } }, () => {
|
||||
this.debouncedUpdateUsers();
|
||||
});
|
||||
|
||||
const { count, results } = userStore.getSearchResult();
|
||||
|
||||
const authorizedToViewUsers = isUserActionAllowed(REQUIRED_PERMISSION_TO_VIEW_USERS);
|
||||
const isAuthorizedToViewUsers = isUserActionAllowed(REQUIRED_PERMISSION_TO_VIEW_USERS);
|
||||
|
||||
return (
|
||||
<PageErrorHandlingWrapper
|
||||
|
|
@ -179,100 +138,126 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
itemNotFoundMessage={`User with id=${id} is not found. Please select user from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('root', 'TEST-users-page')}>
|
||||
<div className={cx('users-header')}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline' }}>
|
||||
<div>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Users</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
{authorizedToViewUsers && (
|
||||
<Text type="secondary">
|
||||
All Grafana users listed below to set notification preferences. To manage permissions or add
|
||||
new users, please visit{' '}
|
||||
<a href="/admin/users" target="_blank">
|
||||
Grafana user management
|
||||
</a>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PluginLink query={{ page: 'users', id: 'me' }}>
|
||||
<Button variant="primary" icon="user">
|
||||
View my profile
|
||||
</Button>
|
||||
</PluginLink>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('users-header')}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline' }}>
|
||||
<div>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Users</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
{isAuthorizedToViewUsers && (
|
||||
<Text type="secondary">
|
||||
All Grafana users listed below to set notification preferences. To manage permissions or add new
|
||||
users, please visit{' '}
|
||||
<a href="/admin/users" target="_blank">
|
||||
Grafana user management
|
||||
</a>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{authorizedToViewUsers ? (
|
||||
<>
|
||||
<div className={cx('user-filters-container')}>
|
||||
<UsersFilters
|
||||
className={cx('users-filters')}
|
||||
value={usersFilters}
|
||||
onChange={this.handleUsersFiltersChange}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="times"
|
||||
onClick={handleClear}
|
||||
className={cx('searchIntegrationClear')}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<GTable
|
||||
data-testid="users-table"
|
||||
emptyText={initialUsersLoaded ? 'No users found' : 'Loading...'}
|
||||
rowKey="pk"
|
||||
data={results}
|
||||
columns={columns}
|
||||
rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Alert
|
||||
/* @ts-ignore */
|
||||
title={
|
||||
<>
|
||||
{generateMissingPermissionMessage(REQUIRED_PERMISSION_TO_VIEW_USERS)} to be able to view OnCall
|
||||
users. <PluginLink query={{ page: 'users', id: 'me' }}>Click here</PluginLink> to open your
|
||||
profile
|
||||
</>
|
||||
}
|
||||
data-testid="view-users-missing-permission-message"
|
||||
severity="info"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
|
||||
<PluginLink query={{ page: 'users', id: 'me' }}>
|
||||
<Button variant="primary" icon="user" data-testid="users-view-my-profile">
|
||||
View my profile
|
||||
</Button>
|
||||
</PluginLink>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{this.renderContentIfAuthorized(isAuthorizedToViewUsers)}
|
||||
|
||||
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
|
||||
</div>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
handleChangePage = (page: number) => {
|
||||
this.setState({ page }, this.updateUsers);
|
||||
};
|
||||
renderContentIfAuthorized(authorizedToViewUsers: boolean) {
|
||||
const {
|
||||
store: { userStore },
|
||||
} = this.props;
|
||||
const { usersFilters, page, initialUsersLoaded, userPkToEdit } = this.state;
|
||||
|
||||
const { count, results } = userStore.getSearchResult();
|
||||
const columns = this.getTableColumns();
|
||||
|
||||
const handleClear = () =>
|
||||
this.setState({ usersFilters: { searchTerm: '' } }, () => {
|
||||
this.debouncedUpdateUsers();
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{authorizedToViewUsers ? (
|
||||
<>
|
||||
<div className={cx('user-filters-container')} data-testid="users-filters">
|
||||
<UsersFilters
|
||||
className={cx('users-filters')}
|
||||
value={usersFilters}
|
||||
onChange={this.handleUsersFiltersChange}
|
||||
/>
|
||||
<Button variant="secondary" icon="times" onClick={handleClear} className={cx('searchIntegrationClear')}>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<GTable
|
||||
data-testid="users-table"
|
||||
emptyText={initialUsersLoaded ? 'No users found' : 'Loading...'}
|
||||
rowKey="pk"
|
||||
data={results}
|
||||
columns={columns}
|
||||
rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Alert
|
||||
title={
|
||||
(
|
||||
<div data-testid="users-missing-permissions">
|
||||
<Text type="primary">
|
||||
{generateMissingPermissionMessage(REQUIRED_PERMISSION_TO_VIEW_USERS)} to be able to view OnCall
|
||||
users. <PluginLink query={{ page: 'users', id: 'me' }}>Click here</PluginLink> to open your profile
|
||||
</Text>
|
||||
</div>
|
||||
) as any
|
||||
}
|
||||
data-testid="view-users-missing-permission-message"
|
||||
severity="info"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle = (user: UserType) => {
|
||||
const {
|
||||
store: { userStore },
|
||||
} = this.props;
|
||||
const isCurrent = userStore.currentUserPk === user.pk;
|
||||
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<Avatar className={cx('user-avatar')} size="large" src={user.avatar} />
|
||||
<div>
|
||||
<div>{user.username}</div>
|
||||
<Text type="secondary">{user.email}</Text>
|
||||
<div
|
||||
className={cx({
|
||||
'current-user': isCurrent,
|
||||
'other-user': !isCurrent,
|
||||
})}
|
||||
>
|
||||
<div data-testid="users-username">{user.username}</div>
|
||||
<Text type="secondary" data-testid="users-email">
|
||||
{user.email}
|
||||
</Text>
|
||||
<br />
|
||||
<Text type="secondary">{user.verified_phone_number}</Text>
|
||||
<Text type="secondary" data-testid="users-phone-number">
|
||||
{user.verified_phone_number}
|
||||
</Text>
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
|
|
@ -311,7 +296,8 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
<WithPermissionControlTooltip userAction={action}>
|
||||
<Button
|
||||
className={cx({
|
||||
'TEST-edit-my-own-settings-button': isCurrent,
|
||||
'edit-my-profile-button': isCurrent,
|
||||
'edit-other-profile-button': !isCurrent,
|
||||
})}
|
||||
fill="text"
|
||||
>
|
||||
|
|
@ -324,7 +310,11 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
};
|
||||
|
||||
renderStatus = (user: UserType) => {
|
||||
const { store } = this.props;
|
||||
const {
|
||||
store,
|
||||
store: { organizationStore, telegramChannelStore },
|
||||
} = this.props;
|
||||
|
||||
if (user.hidden_fields === true) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -345,8 +335,7 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
phone_verified = false;
|
||||
switch (user.cloud_connection_status) {
|
||||
case 0:
|
||||
// Cloud is not connected, no need to show warning to the user
|
||||
break;
|
||||
break; // Cloud is not connected, no need to show warning to the user
|
||||
case 1:
|
||||
warnings.push('User not matched with cloud');
|
||||
break;
|
||||
|
|
@ -354,21 +343,18 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
warnings.push('Phone number is not verified in Grafana Cloud');
|
||||
break;
|
||||
case 3:
|
||||
// Phone is verified in Grafana Cloud, no need to show warning to the user
|
||||
phone_verified = true;
|
||||
phone_verified = true; // Phone is verified in Grafana Cloud, no need to show warning to the user
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (!phone_verified) {
|
||||
warnings.push('Phone not verified');
|
||||
}
|
||||
} else if (!phone_verified) {
|
||||
warnings.push('Phone not verified');
|
||||
}
|
||||
|
||||
if (store.organizationStore.currentOrganization.slack_team_identity && !user.slack_user_identity) {
|
||||
if (organizationStore.currentOrganization.slack_team_identity && !user.slack_user_identity) {
|
||||
warnings.push('Slack profile is not connected');
|
||||
}
|
||||
|
||||
let telegramChannelsExist = store.telegramChannelStore.currentTeamToTelegramChannel?.length > 0;
|
||||
let telegramChannelsExist = telegramChannelStore.currentTeamToTelegramChannel?.length > 0;
|
||||
|
||||
if (store.hasFeature(AppFeature.Telegram) && telegramChannelsExist && !user.telegram_configuration) {
|
||||
warnings.push('Telegram profile is not connected');
|
||||
|
|
@ -397,6 +383,44 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
);
|
||||
};
|
||||
|
||||
getTableColumns(): Array<{ width: string; key: string; title?: string; render }> {
|
||||
return [
|
||||
{
|
||||
width: '25%',
|
||||
key: 'username',
|
||||
title: 'User',
|
||||
render: this.renderTitle,
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
title: 'Status',
|
||||
key: 'note',
|
||||
render: this.renderStatus,
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
title: 'Default Notifications',
|
||||
key: 'notifications-chain',
|
||||
render: this.renderNotificationsChain,
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
title: 'Important Notifications',
|
||||
key: 'important-notifications-chain',
|
||||
render: this.renderImportantNotificationsChain,
|
||||
},
|
||||
{
|
||||
width: '5%',
|
||||
key: 'buttons',
|
||||
render: this.renderButtons,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
handleChangePage = (page: number) => {
|
||||
this.setState({ page }, this.updateUsers);
|
||||
};
|
||||
|
||||
debouncedUpdateUsers = debounce(this.updateUsers, 500);
|
||||
|
||||
handleUsersFiltersChange = (usersFilters: any) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "@grafana/toolkit/src/config/tsconfig.plugin.json",
|
||||
"include": ["src", "frontend_enterprise/src", "integration-tests", "playwright.config.ts"],
|
||||
"include": ["src", "frontend_enterprise/src", "e2e-tests", "playwright.config.ts"],
|
||||
"types": ["node", "@emotion/core"],
|
||||
"compilerOptions": {
|
||||
"rootDirs": ["src", "frontend_enterprise/src"],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue