Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
brojd committed Jun 5, 2024
1 parent b00f1c4 commit 3aa104c
Show file tree
Hide file tree
Showing 14 changed files with 132 additions and 115 deletions.
8 changes: 1 addition & 7 deletions grafana-plugin/e2e-tests/globalSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,9 @@ import {
GRAFANA_VIEWER_USERNAME,
IS_CLOUD,
IS_OPEN_SOURCE,
OrgRole,
} from './utils/constants';

enum OrgRole {
None = 'None',
Viewer = 'Viewer',
Editor = 'Editor',
Admin = 'Admin',
}

type UserCreationSettings = {
adminAuthedRequest: APIRequestContext;
role: OrgRole;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,77 +34,58 @@ Then OnCall loads as usual
*/

import { test, expect } from '../fixtures';
import { clickButton } from '../utils/forms';
import { GRAFANA_ADMIN_USERNAME, OrgRole } from '../utils/constants';
import { goToGrafanaPage, goToOnCallPage } from '../utils/navigation';
import { createGrafanaUser } from '../utils/users';
import { createGrafanaUser, reloginAndWaitTillGrafanaIsLoaded } from '../utils/users';

test.describe('Plugin initialization', () => {
test('Plugin OnCall pages work for new viewer user right away', async ({ adminRolePage: { page } }) => {
// Create new viewer user
// Create new editor user and login as new user
const USER_NAME = `viewer-${new Date().getTime()}`;
await createGrafanaUser(page, USER_NAME);
await createGrafanaUser({ page, username: USER_NAME, role: OrgRole.Viewer });
await reloginAndWaitTillGrafanaIsLoaded({ page, username: USER_NAME });

// Login as new user
await goToGrafanaPage(page, '/logout');
await page.getByLabel('Email or username').fill(USER_NAME);
await page.getByLabel(/Password/).fill(USER_NAME);
await clickButton({ page, buttonText: 'Log in' });

// Wait till Grafana home page is loaded and start tracking HTTP response codes
await page.getByText('Welcome to Grafana').waitFor();
await page.waitForLoadState('networkidle');
// Start watching for HTTP responses
const networkResponseStatuses: number[] = [];
page.on('requestfinished', async (request) => networkResponseStatuses.push((await request.response()).status()));

// Go to OnCall and assert that none of the requests failed
await goToOnCallPage(page, 'alert-groups');
await page.waitForLoadState('networkidle');
const allRequestsPassed = networkResponseStatuses.every(
(status) => `${status}`.startsWith('2') || `${status}`.startsWith('3')
);
expect(allRequestsPassed).toBeTruthy();

// ...and user sees content of alert groups page
// ...as well as that user sees content of alert groups page
await expect(page.getByText('No alert groups found')).toBeVisible();
});

test('Extension registered by OnCall plugin works for new editor user right away', async ({
adminRolePage: { page },
}) => {
// Create new editor user
const USER_NAME = `editor-${new Date().getTime()}`;
await createGrafanaUser(page, USER_NAME);
await clickButton({ page, buttonText: 'Create user' });
await clickButton({ page, buttonText: 'Change role' });
await page
.locator('div')
.filter({ hasText: /^Viewer$/ })
.nth(1)
.click();
await page.getByText(/Editor/).click();
await clickButton({ page, buttonText: 'Save' });

// Login as new user
await goToGrafanaPage(page, '/logout');
await page.getByLabel('Email or username').fill(USER_NAME);
await page.getByLabel(/Password/).fill(USER_NAME);
await clickButton({ page, buttonText: 'Log in' });
// Login again as admin
await reloginAndWaitTillGrafanaIsLoaded({ page, username: GRAFANA_ADMIN_USERNAME });

// Wait till Grafana home page is loaded and start tracking HTTP response codes
await page.getByText('Welcome to Grafana').waitFor();
// Create new editor user and login as new user
const USER_NAME = `editor-${new Date().getTime()}`;
await createGrafanaUser({ page, username: USER_NAME, role: OrgRole.Editor });
await page.waitForLoadState('networkidle');
await reloginAndWaitTillGrafanaIsLoaded({ page, username: USER_NAME });

// Start watching for HTTP responses
const networkResponseStatuses: number[] = [];
page.on('requestfinished', async (request) => networkResponseStatuses.push((await request.response()).status()));

// Go to profile -> IRM tab where OnCall plugin extension is registered
// Go to profile -> IRM tab where OnCall plugin extension is registered and assert that none of the requests failed
await goToGrafanaPage(page, '/profile?tab=irm');
await page.waitForLoadState('networkidle');
const allRequestsPassed = networkResponseStatuses.every(
(status) => `${status}`.startsWith('2') || `${status}`.startsWith('3')
);
expect(allRequestsPassed).toBeTruthy();

console.log(networkResponseStatuses);

// ...and user sees content of alert groups page
// ...as well as that user sees content of the extension
const extensionContentText = page.getByText('Please connect Grafana Cloud OnCall to use the mobile app');
await extensionContentText.waitFor();
await expect(extensionContentText).toBeVisible();
Expand Down
7 changes: 7 additions & 0 deletions grafana-plugin/e2e-tests/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ export const GRAFANA_ADMIN_PASSWORD = process.env.GRAFANA_ADMIN_PASSWORD || 'onc

export const IS_OPEN_SOURCE = (process.env.IS_OPEN_SOURCE || 'true').toLowerCase() === 'true';
export const IS_CLOUD = !IS_OPEN_SOURCE;

export enum OrgRole {
None = 'None',
Viewer = 'Viewer',
Editor = 'Editor',
Admin = 'Admin',
}
34 changes: 32 additions & 2 deletions grafana-plugin/e2e-tests/utils/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Page, expect } from '@playwright/test';
import { clickButton } from './forms';

import { OrgRole } from './constants';
import { clickButton } from './forms';
import { goToGrafanaPage, goToOnCallPage } from './navigation';

export async function accessProfileTabs(page: Page, tabs: string[], hasAccess: boolean) {
Expand Down Expand Up @@ -45,11 +46,40 @@ export async function viewUsers(page: Page, isAllowedToView = true): Promise<voi
}
}

export const createGrafanaUser = async (page: Page, username: string): Promise<void> => {
export const createGrafanaUser = async ({
page,
username,
role = OrgRole.Viewer,
}: {
page: Page;
username: string;
role?: OrgRole;
}): Promise<void> => {
await goToGrafanaPage(page, '/admin/users');
await page.getByRole('link', { name: 'New user' }).click();
await page.getByLabel('Name *').fill(username);
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password *').fill(username);
await clickButton({ page, buttonText: 'Create user' });

if (role !== OrgRole.Viewer) {
await clickButton({ page, buttonText: 'Change role' });
await page
.locator('div')
.filter({ hasText: /^Viewer$/ })
.nth(1)
.click();
await page.getByText(new RegExp(role)).click();
await clickButton({ page, buttonText: 'Save' });
}
};

export const reloginAndWaitTillGrafanaIsLoaded = async ({ page, username }: { page: Page; username: string }) => {
await goToGrafanaPage(page, '/logout');
await page.getByLabel('Email or username').fill(username);
await page.getByLabel(/Password/).fill(username);
await clickButton({ page, buttonText: 'Log in' });

await page.getByText('Welcome to Grafana').waitFor();
await page.waitForLoadState('networkidle');
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { locationUtil, PluginExtensionLink, PluginExtensionTypes } from '@grafan
import { IconName, Menu } from '@grafana/ui';

import { PluginBridge, SupportedPlugin } from 'components/PluginBridge/PluginBridge';
import { PLUGIN_ID } from 'utils/consts';
import { truncateTitle } from 'utils/string';

type Props = {
Expand Down Expand Up @@ -68,7 +69,7 @@ function DeclareIncidentMenuItem({ extensions, declareIncidentLink, grafanaIncid
icon: 'fire',
category: 'Incident',
title: 'Declare incident',
pluginId: 'grafana-oncall-app',
pluginId: PLUGIN_ID,
} as Partial<PluginExtensionLink>,
])}
</Menu.Group>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ import { Block } from 'components/GBlock/Block';
import { PluginLink } from 'components/PluginLink/PluginLink';
import { Text } from 'components/Text/Text';
import { WithPermissionControlDisplay } from 'containers/WithPermissionControl/WithPermissionControlDisplay';
import { ActionKey } from 'models/loader/action-keys';

Check warning on line 12 in grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx

View workflow job for this annotation

GitHub Actions / Lint, test, and build frontend

'ActionKey' is defined but never used
import { UserHelper } from 'models/user/user.helpers';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { AppFeature } from 'state/features';
import { RootStore, rootStore as store } from 'state/rootStore';
import { UserActions } from 'utils/authorization/authorization';
import { useInitializePlugin, useOnMount } from 'utils/hooks';

Check warning on line 18 in grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx

View workflow job for this annotation

GitHub Actions / Lint, test, and build frontend

'useOnMount' is defined but never used
import { isMobile, openErrorNotification, openNotification, openWarningNotification } from 'utils/utils';

import styles from './MobileAppConnection.module.scss';
import { DisconnectButton } from './parts/DisconnectButton/DisconnectButton';
import { DownloadIcons } from './parts/DownloadIcons/DownloadIcons';
import { LinkLoginButton } from './parts/LinkLoginButton/LinkLoginButton';
import { QRCode } from './parts/QRCode/QRCode';
import { useInitializePlugin } from 'utils/hooks';

const cx = cn.bind(styles);

Expand Down Expand Up @@ -365,11 +366,13 @@ function QRLoading() {

export const MobileAppConnectionWrapper: React.FC<{}> = observer(() => {
const { userStore } = store;
const {} = useInitializePlugin({ forceReinstall: true });
const { isInitialized } = useInitializePlugin();

useEffect(() => {
loadData();
}, []);
if (isInitialized) {
loadData();
}
}, [isInitialized]);

const loadData = async () => {
if (!store.isBasicDataLoaded) {
Expand All @@ -381,7 +384,7 @@ export const MobileAppConnectionWrapper: React.FC<{}> = observer(() => {
}
};

if (store.isBasicDataLoaded && userStore.currentUserPk) {
if (isInitialized && store.isBasicDataLoaded && userStore.currentUserPk) {
return <MobileAppConnection userPk={userStore.currentUserPk} />;
}

Expand Down
1 change: 1 addition & 0 deletions grafana-plugin/src/models/loader/action-keys.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum ActionKey {
INITIALIZE_PLUGIN = 'INITIALIZE_PLUGIN',
UPDATE_INTEGRATION = 'UPDATE_INTEGRATION',
ADD_NEW_COLUMN_TO_ALERT_GROUP = 'ADD_NEW_COLUMN_TO_ALERT_GROUP',
REMOVE_COLUMN_FROM_ALERT_GROUP = 'REMOVE_COLUMN_FROM_ALERT_GROUP',
Expand Down
6 changes: 3 additions & 3 deletions grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ import { getQueryParams, isTopNavbar } from './GrafanaPluginRootPage.helpers';

import grafanaGlobalStyle from '!raw-loader!assets/style/grafanaGlobalStyles.css';

export const GrafanaPluginRootPage = (props: AppRootProps) => {
const { isInitialized } = useInitializePlugin({ appRootProps: props });
export const GrafanaPluginRootPage = observer((props: AppRootProps) => {
const { isInitialized } = useInitializePlugin();

useOnMount(() => {
FaroHelper.initializeFaro(getOnCallApiUrl(props.meta));
Expand All @@ -66,7 +66,7 @@ export const GrafanaPluginRootPage = (props: AppRootProps) => {
)}
</ErrorBoundary>
);
};
});

export const Root = observer((props: AppRootProps) => {
const { isBasicDataLoaded, loadBasicData, loadMasterData, pageTitle } = useStore();
Expand Down
49 changes: 38 additions & 11 deletions grafana-plugin/src/state/rootBaseStore/RootBaseStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { GlobalSettingStore } from 'models/global_setting/global_setting';
import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
import { HeartbeatStore } from 'models/heartbeat/heartbeat';
import { LabelStore } from 'models/label/label';
import { ActionKey } from 'models/loader/action-keys';
import { LoaderStore } from 'models/loader/loader';
import { MSTeamsChannelStore } from 'models/msteams_channel/msteams_channel';
import { OrganizationStore } from 'models/organization/organization';
Expand All @@ -33,6 +34,8 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
import { AppFeature } from 'state/features';
import { retryFailingPromises } from 'utils/async';
import { APP_VERSION, CLOUD_VERSION_REGEX, GRAFANA_LICENSE_CLOUD, GRAFANA_LICENSE_OSS } from 'utils/consts';
import { AutoLoadingState } from 'utils/decorators';
import { getIsRunningOpenSourceVersion } from 'utils/utils';

// ------ Dashboard ------ //

Expand All @@ -50,7 +53,7 @@ export class RootBaseStore {
recaptchaSiteKey = '';

@observable
initializationError = '';
isPluginInitialized = false;

@observable
currentlyUndergoingMaintenance = false;
Expand Down Expand Up @@ -107,6 +110,12 @@ export class RootBaseStore {
constructor() {
makeObservable(this);
}

@action.bound
setIsPluginInitialized(value: boolean) {
this.isPluginInitialized = value;
}

@action.bound
loadBasicData = async () => {
const updateFeatures = async () => {
Expand Down Expand Up @@ -177,18 +186,36 @@ export class RootBaseStore {
this.pageTitle = title;
}

@action
async removeSlackIntegration() {
await this.slackStore.removeSlackIntegration();
}

@action
async installSlackIntegration() {
await this.slackStore.installSlackIntegration();
}

@action.bound
async getApiUrlForSettings() {
return this.onCallApiUrl;
}

@AutoLoadingState(ActionKey.INITIALIZE_PLUGIN)
@action.bound
async initializePlugin() {
const IS_OPEN_SOURCE = getIsRunningOpenSourceVersion();

// create oncall api token and save in plugin settings
const install = async () => {
await makeRequest(`/plugin${IS_OPEN_SOURCE ? '/self-hosted' : ''}/install`, {
method: 'POST',
});
};

// trigger users sync
try {
// TODO: once we improve backend we should get rid of token_ok check and call install() only in catch block
const { token_ok } = await makeRequest(`/plugin/status`, {
method: 'POST',
});
if (!token_ok) {
await install();
}
} catch (_err) {
await install();
}

this.setIsPluginInitialized(true);
}
}
4 changes: 2 additions & 2 deletions grafana-plugin/src/utils/authorization/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { OrgRole } from '@grafana/data';
import { config } from '@grafana/runtime';
import { contextSrv } from 'grafana/app/core/core';

const ONCALL_PERMISSION_PREFIX = 'grafana-oncall-app';
import { PLUGIN_ID } from 'utils/consts';

export type UserAction = {
permission: string;
Expand Down Expand Up @@ -110,7 +110,7 @@ export const generateMissingPermissionMessage = (permission: UserAction): string
`You are missing the ${determineRequiredAuthString(permission)}`;

export const generatePermissionString = (resource: Resource, action: Action, includePrefix: boolean): string =>
`${includePrefix ? `${ONCALL_PERMISSION_PREFIX}.` : ''}${resource}:${action}`;
`${includePrefix ? `${PLUGIN_ID}.` : ''}${resource}:${action}`;

const constructAction = (
resource: Resource,
Expand Down
5 changes: 3 additions & 2 deletions grafana-plugin/src/utils/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export const BREAKPOINT_TABS = 1024;
// Default redirect page
export const DEFAULT_PAGE = 'alert-groups';

export const PLUGIN_ROOT = '/a/grafana-oncall-app';
export const PLUGIN_ID = 'grafana-oncall-app';
export const PLUGIN_ROOT = `/a/${PLUGIN_ID}`;

// Environment options list for onCallApiUrl
export const ONCALL_PROD = 'https://oncall-prod-us-central-0.grafana.net/oncall';
Expand All @@ -54,7 +55,7 @@ export const getOnCallApiUrl = (meta?: OnCallAppPluginMeta) => {
return undefined;
};

export const getOnCallApiPath = (subpath = '') => `/api/plugins/grafana-oncall-app/resources${subpath}`;
export const getOnCallApiPath = (subpath = '') => `/api/plugins/${PLUGIN_ID}/resources${subpath}`;

// Faro
export const FARO_ENDPOINT_DEV =
Expand Down
Loading

0 comments on commit 3aa104c

Please sign in to comment.