Change permissions used for finishing plugin setup (#2242)
Fixes issue where user not having `plugins:install`permission were unable to complete setup of OnCall. - Check multiple Grafana permissions to complete OnCall setup instead of `plugins:install` since the plugin is already installed at this point - Use the following permissions - `plugins:write` - Plugin setup will write to plugin config - `users:read` - Grafana API key being granted to OnCall will be used to read users from Grafana - `teams:read` - Grafana API key being granted to OnCall will be used to read teams from Grafana - `apikeys:create` - If Grafana API key does not exist it will be created - `apikeys:delete` - If existing Grafana API key does not work it will be deleted and recreated Closes https://github.com/grafana/oncall-private/issues/1925 TODO: - [x] Fix tests
This commit is contained in:
parent
d23e2f44da
commit
dd713bac55
11 changed files with 418 additions and 178 deletions
|
|
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
- Change permissions used during setup to better represent actions being taken by @mderynck ([#2242](https://github.com/grafana/oncall/pull/2242))
|
||||
|
||||
## v1.3.1 (2023-06-26)
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ describe('PluginConfigPage', () => {
|
|||
const metaJsonDataOnCallApiUrl = 'onCallApiUrlFromMetaJsonData';
|
||||
|
||||
process.env.ONCALL_API_URL = processEnvOnCallApiUrl;
|
||||
|
||||
window.location.reload = jest.fn();
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce(null);
|
||||
mockSyncDataWithOnCall(License.OSS);
|
||||
|
||||
|
|
@ -302,8 +302,6 @@ describe('PluginConfigPage', () => {
|
|||
// click the confirm button within the modal, which actually triggers the callback
|
||||
await userEvent.click(screen.getByText('Remove'));
|
||||
|
||||
await screen.findByTestId(successful ? PLUGIN_CONFIGURATION_FORM_DATA_ID : STATUS_MESSAGE_BLOCK_DATA_ID);
|
||||
|
||||
// assertions
|
||||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledTimes(1);
|
||||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledWith(metaJsonDataOnCallApiUrl);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Button, Label, Legend, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { Button, HorizontalGroup, Label, Legend, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { OnCallPluginConfigPageProps } from 'types';
|
||||
|
||||
import logo from 'img/logo.svg';
|
||||
import PluginState, { PluginStatusResponseBase } from 'state/plugin';
|
||||
import { GRAFANA_LICENSE_OSS } from 'utils/consts';
|
||||
import { FALLBACK_LICENSE, GRAFANA_LICENSE_OSS } from 'utils/consts';
|
||||
|
||||
import ConfigurationForm from './parts/ConfigurationForm';
|
||||
import RemoveCurrentConfigurationButton from './parts/RemoveCurrentConfigurationButton';
|
||||
|
|
@ -75,13 +74,13 @@ const PluginConfigPage: FC<OnCallPluginConfigPageProps> = ({
|
|||
const pluginMetaOnCallApiUrl = jsonData?.onCallApiUrl;
|
||||
const processEnvOnCallApiUrl = process.env.ONCALL_API_URL; // don't destructure this, will break how webpack supplies this
|
||||
const onCallApiUrl = pluginMetaOnCallApiUrl || processEnvOnCallApiUrl;
|
||||
const licenseType = pluginIsConnected?.license;
|
||||
const licenseType = pluginIsConnected?.license || FALLBACK_LICENSE;
|
||||
|
||||
const resetQueryParams = useCallback(() => removePluginConfiguredQueryParams(pluginIsEnabled), [pluginIsEnabled]);
|
||||
|
||||
const triggerDataSyncWithOnCall = useCallback(async () => {
|
||||
resetMessages();
|
||||
setSyncingPlugin(true);
|
||||
setSyncError(null);
|
||||
|
||||
const syncDataResponse = await PluginState.syncDataWithOnCall(onCallApiUrl);
|
||||
|
||||
|
|
@ -144,35 +143,25 @@ const PluginConfigPage: FC<OnCallPluginConfigPageProps> = ({
|
|||
}
|
||||
}, [pluginMetaOnCallApiUrl, processEnvOnCallApiUrl, onCallApiUrl, pluginConfiguredRedirect]);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
const resetMessages = useCallback(() => {
|
||||
setPluginResetError(null);
|
||||
setPluginConnectionCheckError(null);
|
||||
setPluginIsConnected(null);
|
||||
setSyncError(null);
|
||||
}, []);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
resetMessages();
|
||||
resetQueryParams();
|
||||
}, [resetQueryParams]);
|
||||
|
||||
/**
|
||||
* NOTE: there is a possible edge case when resetting the plugin, that would lead to an error message being shown
|
||||
* (which could be fixed by just reloading the page)
|
||||
* This would happen if the user removes the plugin configuration, leaves the page, then comes back to the plugin
|
||||
* configuration.
|
||||
*
|
||||
* This is because the props being passed into this component wouldn't reflect the actual plugin
|
||||
* provisioning state. The props would still have onCallApiUrl set in the plugin jsonData, so when we make the API
|
||||
* call to check the plugin state w/ OnCall API the plugin-proxy would return a 502 Bad Gateway because the actual
|
||||
* provisioned plugin doesn't know about the onCallApiUrl.
|
||||
*
|
||||
* This could be fixed by instead of passing in the plugin provisioning information as props always fetching it
|
||||
* when this component renders (via a useEffect). We probably don't need to worry about this because it should happen
|
||||
* very rarely, if ever
|
||||
*/
|
||||
const triggerPluginReset = useCallback(async () => {
|
||||
setResettingPlugin(true);
|
||||
resetState();
|
||||
|
||||
try {
|
||||
await PluginState.resetPlugin();
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
// this should rarely, if ever happen, but we should handle the case nevertheless
|
||||
setPluginResetError('There was an error resetting your plugin, try again.');
|
||||
|
|
@ -186,6 +175,15 @@ const PluginConfigPage: FC<OnCallPluginConfigPageProps> = ({
|
|||
[resettingPlugin, triggerPluginReset]
|
||||
);
|
||||
|
||||
const ReconfigurePluginButtons = () => (
|
||||
<HorizontalGroup>
|
||||
<Button variant="primary" onClick={triggerDataSyncWithOnCall} size="md">
|
||||
Retry Sync
|
||||
</Button>
|
||||
{licenseType === GRAFANA_LICENSE_OSS ? <RemoveConfigButton /> : null}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
|
||||
let content: React.ReactNode;
|
||||
|
||||
if (checkingIfPluginIsConnected) {
|
||||
|
|
@ -196,16 +194,14 @@ const PluginConfigPage: FC<OnCallPluginConfigPageProps> = ({
|
|||
content = (
|
||||
<>
|
||||
<StatusMessageBlock text={pluginConnectionCheckError || pluginResetError} />
|
||||
<RemoveConfigButton />
|
||||
<ReconfigurePluginButtons />
|
||||
</>
|
||||
);
|
||||
} else if (syncError) {
|
||||
content = (
|
||||
<>
|
||||
<StatusMessageBlock text={syncError} />
|
||||
<Button variant="primary" onClick={triggerDataSyncWithOnCall} size="md">
|
||||
Retry Sync
|
||||
</Button>
|
||||
<ReconfigurePluginButtons />
|
||||
</>
|
||||
);
|
||||
} else if (!pluginIsConnected) {
|
||||
|
|
@ -228,8 +224,8 @@ const PluginConfigPage: FC<OnCallPluginConfigPageProps> = ({
|
|||
{pluginIsConnected ? (
|
||||
<>
|
||||
<p>
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking the{' '}
|
||||
<img alt="Grafana OnCall Logo" src={logo} width={18} /> icon over there 👈
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking OnCall under Alerts & IRM in the navigation over
|
||||
there 👈
|
||||
</p>
|
||||
<StatusMessageBlock
|
||||
text={`Connected to OnCall (${pluginIsConnected.version}, ${pluginIsConnected.license})`}
|
||||
|
|
|
|||
|
|
@ -19,16 +19,39 @@ exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonDa
|
|||
ohhh nooo an error msg from self hosted install plugin
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-1ed0qk5-button"
|
||||
type="button"
|
||||
<div
|
||||
class="css-ve64a7-horizontal-group"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="css-z53gi5-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Retry Sync
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
<button
|
||||
class="css-1ed0qk5-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -152,16 +175,39 @@ exports[`PluginConfigPage If onCallApiUrl is set, and checkIfPluginIsConnected r
|
|||
ohhh nooo a plugin connection error
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-1ed0qk5-button"
|
||||
type="button"
|
||||
<div
|
||||
class="css-ve64a7-horizontal-group"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="css-z53gi5-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Retry Sync
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
<button
|
||||
class="css-1ed0qk5-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -173,14 +219,7 @@ exports[`PluginConfigPage It doesn't make any network calls if the plugin config
|
|||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking the
|
||||
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
width="18"
|
||||
/>
|
||||
icon over there 👈
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking OnCall under Alerts & IRM in the navigation over there 👈
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
|
|
@ -212,14 +251,7 @@ exports[`PluginConfigPage OnCallApiUrl is set, and syncDataWithOnCall does not r
|
|||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking the
|
||||
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
width="18"
|
||||
/>
|
||||
icon over there 👈
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking OnCall under Alerts & IRM in the navigation over there 👈
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
|
|
@ -251,14 +283,7 @@ exports[`PluginConfigPage OnCallApiUrl is set, and syncDataWithOnCall does not r
|
|||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking the
|
||||
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
width="18"
|
||||
/>
|
||||
icon over there 👈
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking OnCall under Alerts & IRM in the navigation over there 👈
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
|
|
@ -302,16 +327,39 @@ exports[`PluginConfigPage OnCallApiUrl is set, and syncDataWithOnCall returns an
|
|||
ohhh noooo a sync issue
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-z53gi5-button"
|
||||
type="button"
|
||||
<div
|
||||
class="css-ve64a7-horizontal-group"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
Retry Sync
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="css-z53gi5-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Retry Sync
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
<button
|
||||
class="css-1ed0qk5-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -334,16 +382,39 @@ exports[`PluginConfigPage Plugin reset: successful - false 1`] = `
|
|||
There was an error resetting your plugin, try again.
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-1ed0qk5-button"
|
||||
type="button"
|
||||
<div
|
||||
class="css-ve64a7-horizontal-group"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="css-z53gi5-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Retry Sync
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
<button
|
||||
class="css-1ed0qk5-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,58 +1,58 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PluginState.generateInvalidOnCallApiURLErrorMsg it returns the proper error message - configured through env var: false 1`] = `
|
||||
"Could not communicate with your OnCall API at http://hello.com.
|
||||
Validate that the URL is correct, your OnCall API is running, and that it is accessible from your Grafana instance."
|
||||
"Could not communicate with OnCall API at http://hello.com.
|
||||
Validate that the URL is correct, OnCall API is running, and that it is accessible from your Grafana instance."
|
||||
`;
|
||||
|
||||
exports[`PluginState.generateInvalidOnCallApiURLErrorMsg it returns the proper error message - configured through env var: true 1`] = `
|
||||
"Could not communicate with your OnCall API at http://hello.com (NOTE: your OnCall API URL is currently being taken from process.env of your UI).
|
||||
Validate that the URL is correct, your OnCall API is running, and that it is accessible from your Grafana instance."
|
||||
"Could not communicate with OnCall API at http://hello.com (NOTE: OnCall API URL is currently being taken from process.env of your UI).
|
||||
Validate that the URL is correct, OnCall API is running, and that it is accessible from your Grafana instance."
|
||||
`;
|
||||
|
||||
exports[`PluginState.generateOnCallApiUrlConfiguredThroughEnvVarMsg it returns the proper error message - configured through env var: false 1`] = `""`;
|
||||
|
||||
exports[`PluginState.generateOnCallApiUrlConfiguredThroughEnvVarMsg it returns the proper error message - configured through env var: true 1`] = `" (NOTE: your OnCall API URL is currently being taken from process.env of your UI)"`;
|
||||
exports[`PluginState.generateOnCallApiUrlConfiguredThroughEnvVarMsg it returns the proper error message - configured through env var: true 1`] = `" (NOTE: OnCall API URL is currently being taken from process.env of your UI)"`;
|
||||
|
||||
exports[`PluginState.generateUnknownErrorMsg it returns the proper error message - configured through env var: false 1`] = `
|
||||
"An unknown error occured when trying to install the plugin. Are you sure that your OnCall API URL, http://hello.com, is correct?
|
||||
"An unknown error occurred when trying to install the plugin. Verify OnCall API URL, http://hello.com, is correct?
|
||||
Refresh your page and try again, or try removing your plugin configuration and reconfiguring."
|
||||
`;
|
||||
|
||||
exports[`PluginState.generateUnknownErrorMsg it returns the proper error message - configured through env var: false 2`] = `
|
||||
"An unknown error occured when trying to sync the plugin. Are you sure that your OnCall API URL, http://hello.com, is correct?
|
||||
"An unknown error occurred when trying to sync the plugin. Verify OnCall API URL, http://hello.com, is correct?
|
||||
Refresh your page and try again, or try removing your plugin configuration and reconfiguring."
|
||||
`;
|
||||
|
||||
exports[`PluginState.generateUnknownErrorMsg it returns the proper error message - configured through env var: true 1`] = `
|
||||
"An unknown error occured when trying to install the plugin. Are you sure that your OnCall API URL, http://hello.com, is correct (NOTE: your OnCall API URL is currently being taken from process.env of your UI)?
|
||||
"An unknown error occurred when trying to install the plugin. Verify OnCall API URL, http://hello.com, is correct (NOTE: OnCall API URL is currently being taken from process.env of your UI)?
|
||||
Refresh your page and try again, or try removing your plugin configuration and reconfiguring."
|
||||
`;
|
||||
|
||||
exports[`PluginState.generateUnknownErrorMsg it returns the proper error message - configured through env var: true 2`] = `
|
||||
"An unknown error occured when trying to sync the plugin. Are you sure that your OnCall API URL, http://hello.com, is correct (NOTE: your OnCall API URL is currently being taken from process.env of your UI)?
|
||||
"An unknown error occurred when trying to sync the plugin. Verify OnCall API URL, http://hello.com, is correct (NOTE: OnCall API URL is currently being taken from process.env of your UI)?
|
||||
Refresh your page and try again, or try removing your plugin configuration and reconfiguring."
|
||||
`;
|
||||
|
||||
exports[`PluginState.getHumanReadableErrorFromOnCallError it handles a 400 network error properly - has custom error message: false 1`] = `
|
||||
"An unknown error occured when trying to install the plugin. Are you sure that your OnCall API URL, http://hello.com, is correct (NOTE: your OnCall API URL is currently being taken from process.env of your UI)?
|
||||
"An unknown error occurred when trying to install the plugin. Verify OnCall API URL, http://hello.com, is correct (NOTE: OnCall API URL is currently being taken from process.env of your UI)?
|
||||
Refresh your page and try again, or try removing your plugin configuration and reconfiguring."
|
||||
`;
|
||||
|
||||
exports[`PluginState.getHumanReadableErrorFromOnCallError it handles a 400 network error properly - has custom error message: true 1`] = `"ohhhh nooo an error"`;
|
||||
|
||||
exports[`PluginState.getHumanReadableErrorFromOnCallError it handles a non-400 network error properly - status code: 409 1`] = `
|
||||
"An unknown error occured when trying to install the plugin. Are you sure that your OnCall API URL, http://hello.com, is correct (NOTE: your OnCall API URL is currently being taken from process.env of your UI)?
|
||||
"An unknown error occurred when trying to install the plugin. Verify OnCall API URL, http://hello.com, is correct (NOTE: OnCall API URL is currently being taken from process.env of your UI)?
|
||||
Refresh your page and try again, or try removing your plugin configuration and reconfiguring."
|
||||
`;
|
||||
|
||||
exports[`PluginState.getHumanReadableErrorFromOnCallError it handles a non-400 network error properly - status code: 502 1`] = `
|
||||
"Could not communicate with your OnCall API at http://hello.com (NOTE: your OnCall API URL is currently being taken from process.env of your UI).
|
||||
Validate that the URL is correct, your OnCall API is running, and that it is accessible from your Grafana instance."
|
||||
"Could not communicate with OnCall API at http://hello.com (NOTE: OnCall API URL is currently being taken from process.env of your UI).
|
||||
Validate that the URL is correct, OnCall API is running, and that it is accessible from your Grafana instance."
|
||||
`;
|
||||
|
||||
exports[`PluginState.getHumanReadableErrorFromOnCallError it handles an unknown error properly 1`] = `
|
||||
"An unknown error occured when trying to install the plugin. Are you sure that your OnCall API URL, http://hello.com, is correct (NOTE: your OnCall API URL is currently being taken from process.env of your UI)?
|
||||
"An unknown error occurred when trying to install the plugin. Verify OnCall API URL, http://hello.com, is correct (NOTE: OnCall API URL is currently being taken from process.env of your UI)?
|
||||
Refresh your page and try again, or try removing your plugin configuration and reconfiguring."
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,21 +48,19 @@ class PluginState {
|
|||
static grafanaBackend = getBackendSrv();
|
||||
|
||||
static generateOnCallApiUrlConfiguredThroughEnvVarMsg = (isConfiguredThroughEnvVar: boolean): string =>
|
||||
isConfiguredThroughEnvVar
|
||||
? ' (NOTE: your OnCall API URL is currently being taken from process.env of your UI)'
|
||||
: '';
|
||||
isConfiguredThroughEnvVar ? ' (NOTE: OnCall API URL is currently being taken from process.env of your UI)' : '';
|
||||
|
||||
static generateInvalidOnCallApiURLErrorMsg = (onCallApiUrl: string, isConfiguredThroughEnvVar: boolean): string =>
|
||||
`Could not communicate with your OnCall API at ${onCallApiUrl}${this.generateOnCallApiUrlConfiguredThroughEnvVarMsg(
|
||||
`Could not communicate with OnCall API at ${onCallApiUrl}${this.generateOnCallApiUrlConfiguredThroughEnvVarMsg(
|
||||
isConfiguredThroughEnvVar
|
||||
)}.\nValidate that the URL is correct, your OnCall API is running, and that it is accessible from your Grafana instance.`;
|
||||
)}.\nValidate that the URL is correct, OnCall API is running, and that it is accessible from your Grafana instance.`;
|
||||
|
||||
static generateUnknownErrorMsg = (
|
||||
onCallApiUrl: string,
|
||||
verb: InstallationVerb,
|
||||
isConfiguredThroughEnvVar: boolean
|
||||
): string =>
|
||||
`An unknown error occured when trying to ${verb} the plugin. Are you sure that your OnCall API URL, ${onCallApiUrl}, is correct${this.generateOnCallApiUrlConfiguredThroughEnvVarMsg(
|
||||
`An unknown error occurred when trying to ${verb} the plugin. Verify OnCall API URL, ${onCallApiUrl}, is correct${this.generateOnCallApiUrlConfiguredThroughEnvVarMsg(
|
||||
isConfiguredThroughEnvVar
|
||||
)}?\nRefresh your page and try again, or try removing your plugin configuration and reconfiguring.`;
|
||||
|
||||
|
|
@ -78,7 +76,7 @@ class PluginState {
|
|||
installationVerb,
|
||||
onCallApiUrlIsConfiguredThroughEnvVar
|
||||
);
|
||||
const consoleMsg = `occured while trying to ${installationVerb} the plugin w/ the OnCall backend`;
|
||||
const consoleMsg = `occurred while trying to ${installationVerb} the plugin w/ the OnCall backend`;
|
||||
|
||||
if (isNetworkError(e)) {
|
||||
const { status: statusCode } = e.response;
|
||||
|
|
@ -104,7 +102,7 @@ class PluginState {
|
|||
errorMsg = unknownErrorMsg;
|
||||
}
|
||||
} else {
|
||||
// a non-network related error occured.. this scenario shouldn't occur...
|
||||
// a non-network related error occurred.. this scenario shouldn't occur...
|
||||
console.warn(`An unknown error ${consoleMsg}`, e);
|
||||
errorMsg = unknownErrorMsg;
|
||||
}
|
||||
|
|
@ -121,11 +119,11 @@ class PluginState {
|
|||
|
||||
if (isNetworkError(e)) {
|
||||
// The user likely put in a bogus URL for the OnCall API URL
|
||||
console.warn('An HTTP related error occured while trying to provision the plugin w/ Grafana', e.response);
|
||||
console.warn('An HTTP related error occurred while trying to provision the plugin w/ Grafana', e.response);
|
||||
errorMsg = this.generateInvalidOnCallApiURLErrorMsg(onCallApiUrl, onCallApiUrlIsConfiguredThroughEnvVar);
|
||||
} else {
|
||||
// a non-network related error occured.. this scenario shouldn't occur...
|
||||
console.warn('An unknown error occured while trying to provision the plugin w/ Grafana', e);
|
||||
// a non-network related error occurred.. this scenario shouldn't occur...
|
||||
console.warn('An unknown error occurred while trying to provision the plugin w/ Grafana', e);
|
||||
errorMsg = this.generateUnknownErrorMsg(onCallApiUrl, installationVerb, onCallApiUrlIsConfiguredThroughEnvVar);
|
||||
}
|
||||
return errorMsg;
|
||||
|
|
@ -137,16 +135,20 @@ class PluginState {
|
|||
static updateGrafanaPluginSettings = async (data: UpdateGrafanaPluginSettingsProps, enabled = true) =>
|
||||
this.grafanaBackend.post(this.GRAFANA_PLUGIN_SETTINGS_URL, { ...data, enabled, pinned: true });
|
||||
|
||||
static createGrafanaToken = async () => {
|
||||
const baseUrl = '/api/auth/keys';
|
||||
const keys = await this.grafanaBackend.get(baseUrl);
|
||||
const existingKey = keys.find((key: { id: number; name: string; role: string }) => key.name === 'OnCall');
|
||||
static readonly KEYS_BASE_URL = '/api/auth/keys';
|
||||
|
||||
static getGrafanaToken = async () => {
|
||||
const keys = await this.grafanaBackend.get(this.KEYS_BASE_URL);
|
||||
return keys.find((key: { id: number; name: string; role: string }) => key.name === 'OnCall');
|
||||
};
|
||||
|
||||
static createGrafanaToken = async () => {
|
||||
const existingKey = await this.getGrafanaToken();
|
||||
if (existingKey) {
|
||||
await this.grafanaBackend.delete(`${baseUrl}/${existingKey.id}`);
|
||||
await this.grafanaBackend.delete(`${this.KEYS_BASE_URL}/${existingKey.id}`);
|
||||
}
|
||||
|
||||
return await this.grafanaBackend.post(baseUrl, {
|
||||
return await this.grafanaBackend.post(this.KEYS_BASE_URL, {
|
||||
name: 'OnCall',
|
||||
role: 'Admin',
|
||||
secondsToLive: null,
|
||||
|
|
@ -205,9 +207,27 @@ class PluginState {
|
|||
onCallApiUrlIsConfiguredThroughEnvVar = false
|
||||
): Promise<PluginSyncStatusResponse | string> => {
|
||||
try {
|
||||
/**
|
||||
* Allows the plugin config page to repair settings like the app initialization screen if a user deletes
|
||||
* an API key on accident but leaves the plugin settings intact.
|
||||
*/
|
||||
const existingKey = await this.getGrafanaToken();
|
||||
if (!existingKey) {
|
||||
try {
|
||||
await this.installPlugin();
|
||||
} catch (e) {
|
||||
return this.getHumanReadableErrorFromOnCallError(
|
||||
e,
|
||||
onCallApiUrl,
|
||||
'install',
|
||||
onCallApiUrlIsConfiguredThroughEnvVar
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const startSyncResponse = await makeRequest(`${this.ONCALL_BASE_URL}/sync`, { method: 'POST' });
|
||||
if (typeof startSyncResponse === 'string') {
|
||||
// an error occured trying to initiate the sync
|
||||
// an error occurred trying to initiate the sync
|
||||
return startSyncResponse;
|
||||
}
|
||||
|
||||
|
|
@ -300,11 +320,22 @@ class PluginState {
|
|||
return null;
|
||||
};
|
||||
|
||||
static checkIfBackendIsInMaintenanceMode = async (): Promise<string> => {
|
||||
const response = await makeRequest<PluginIsInMaintenanceModeResponse>('/maintenance-mode-status', {
|
||||
method: 'GET',
|
||||
});
|
||||
return response.currently_undergoing_maintenance_message;
|
||||
static checkIfBackendIsInMaintenanceMode = async (
|
||||
onCallApiUrl: string,
|
||||
onCallApiUrlIsConfiguredThroughEnvVar = false
|
||||
): Promise<PluginIsInMaintenanceModeResponse | string> => {
|
||||
try {
|
||||
return await makeRequest<PluginIsInMaintenanceModeResponse>('/maintenance-mode-status', {
|
||||
method: 'GET',
|
||||
});
|
||||
} catch (e) {
|
||||
return this.getHumanReadableErrorFromOnCallError(
|
||||
e,
|
||||
onCallApiUrl,
|
||||
'install',
|
||||
onCallApiUrlIsConfiguredThroughEnvVar
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static checkIfPluginIsConnected = async (
|
||||
|
|
|
|||
|
|
@ -383,6 +383,7 @@ describe('PluginState.syncDataWithOnCall', () => {
|
|||
const errorMsg = 'asdfasdf';
|
||||
|
||||
makeRequest.mockResolvedValueOnce(errorMsg);
|
||||
PluginState.getGrafanaToken = jest.fn().mockReturnValueOnce({ id: 1 });
|
||||
PluginState.pollOnCallDataSyncStatus = jest.fn();
|
||||
|
||||
// test
|
||||
|
|
@ -403,6 +404,7 @@ describe('PluginState.syncDataWithOnCall', () => {
|
|||
const mockedPollOnCallDataSyncStatusResponse = 'dfjkdfjdf';
|
||||
|
||||
makeRequest.mockResolvedValueOnce(mockedResponse);
|
||||
PluginState.getGrafanaToken = jest.fn().mockReturnValueOnce({ id: 1 });
|
||||
PluginState.pollOnCallDataSyncStatus = jest.fn().mockResolvedValueOnce(mockedPollOnCallDataSyncStatusResponse);
|
||||
|
||||
// test
|
||||
|
|
@ -427,6 +429,7 @@ describe('PluginState.syncDataWithOnCall', () => {
|
|||
const mockedHumanReadableError = 'asdfjkdfjkdfjk';
|
||||
|
||||
makeRequest.mockRejectedValueOnce(mockedError);
|
||||
PluginState.getGrafanaToken = jest.fn().mockReturnValueOnce({ id: 1 });
|
||||
PluginState.pollOnCallDataSyncStatus = jest.fn();
|
||||
PluginState.getHumanReadableErrorFromOnCallError = jest.fn().mockReturnValueOnce(mockedHumanReadableError);
|
||||
|
||||
|
|
@ -663,13 +666,14 @@ describe('PluginState.checkIfBackendIsInMaintenanceMode', () => {
|
|||
// mocks
|
||||
const maintenanceModeMsg = 'asdfljkadsjlfkajsdf';
|
||||
const mockedResp = { currently_undergoing_maintenance_message: maintenanceModeMsg };
|
||||
const onCallApiUrl = 'http://hello.com';
|
||||
makeRequest.mockResolvedValueOnce(mockedResp);
|
||||
|
||||
// test
|
||||
const response = await PluginState.checkIfBackendIsInMaintenanceMode();
|
||||
const response = await PluginState.checkIfBackendIsInMaintenanceMode(onCallApiUrl);
|
||||
|
||||
// assertions
|
||||
expect(response).toEqual(maintenanceModeMsg);
|
||||
expect(response).toEqual(mockedResp);
|
||||
expect(makeRequest).toHaveBeenCalledTimes(1);
|
||||
expect(makeRequest).toHaveBeenCalledWith('/maintenance-mode-status', { method: 'GET' });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { OrgRole } from '@grafana/data';
|
||||
import { contextSrv } from 'grafana/app/core/core';
|
||||
import { action, observable } from 'mobx';
|
||||
import moment from 'moment-timezone';
|
||||
import qs from 'query-string';
|
||||
|
|
@ -32,8 +34,7 @@ import { UserGroupStore } from 'models/user_group/user_group';
|
|||
import { makeRequest } from 'network';
|
||||
import { AppFeature } from 'state/features';
|
||||
import PluginState from 'state/plugin';
|
||||
import { isUserActionAllowed, UserActions } from 'utils/authorization';
|
||||
import { GRAFANA_LICENSE_OSS } from 'utils/consts';
|
||||
import { APP_VERSION, CLOUD_VERSION_REGEX, GRAFANA_LICENSE_CLOUD, GRAFANA_LICENSE_OSS } from 'utils/consts';
|
||||
|
||||
// ------ Dashboard ------ //
|
||||
|
||||
|
|
@ -162,13 +163,15 @@ export class RootBaseStore {
|
|||
return this.setupPluginError('🚫 Plugin has not been initialized');
|
||||
}
|
||||
|
||||
const isInMaintenanceMode = await PluginState.checkIfBackendIsInMaintenanceMode();
|
||||
if (isInMaintenanceMode !== null) {
|
||||
const maintenanceMode = await PluginState.checkIfBackendIsInMaintenanceMode(this.onCallApiUrl);
|
||||
if (typeof maintenanceMode === 'string') {
|
||||
return this.setupPluginError(maintenanceMode);
|
||||
} else if (maintenanceMode.currently_undergoing_maintenance_message) {
|
||||
this.currentlyUndergoingMaintenance = true;
|
||||
return this.setupPluginError(`🚧 ${isInMaintenanceMode} 🚧`);
|
||||
return this.setupPluginError(`🚧 ${maintenanceMode.currently_undergoing_maintenance_message} 🚧`);
|
||||
}
|
||||
|
||||
// at this point we know the plugin is provionsed
|
||||
// at this point we know the plugin is provisioned
|
||||
const pluginConnectionStatus = await PluginState.checkIfPluginIsConnected(this.onCallApiUrl);
|
||||
if (typeof pluginConnectionStatus === 'string') {
|
||||
return this.setupPluginError(pluginConnectionStatus);
|
||||
|
|
@ -178,28 +181,38 @@ export class RootBaseStore {
|
|||
|
||||
if (is_user_anonymous) {
|
||||
return this.setupPluginError(
|
||||
'😞 Unfortunately Grafana OnCall is available for authorized users only, please sign in to proceed.'
|
||||
'😞 Grafana OnCall is available for authorized users only, please sign in to proceed.'
|
||||
);
|
||||
} else if (!is_installed || !token_ok) {
|
||||
if (!allow_signup) {
|
||||
return this.setupPluginError('🚫 OnCall has temporarily disabled signup of new users. Please try again later.');
|
||||
}
|
||||
|
||||
if (!isUserActionAllowed(UserActions.PluginsInstall)) {
|
||||
return this.setupPluginError(
|
||||
'🚫 An Admin in your organization must sign on and setup OnCall before it can be used'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* this will install AND sync the necessary data
|
||||
* the sync is done automatically by the /plugin/install OnCall API endpoint
|
||||
* therefore there is no need to trigger an additional/separate sync, nor poll a status
|
||||
*/
|
||||
await PluginState.installPlugin();
|
||||
} catch (e) {
|
||||
return this.setupPluginError(PluginState.getHumanReadableErrorFromOnCallError(e, this.onCallApiUrl, 'install'));
|
||||
const missingPermissions = this.checkMissingSetupPermissions();
|
||||
if (missingPermissions.length === 0) {
|
||||
try {
|
||||
/**
|
||||
* this will install AND sync the necessary data
|
||||
* the sync is done automatically by the /plugin/install OnCall API endpoint
|
||||
* therefore there is no need to trigger an additional/separate sync, nor poll a status
|
||||
*/
|
||||
await PluginState.installPlugin();
|
||||
} catch (e) {
|
||||
return this.setupPluginError(
|
||||
PluginState.getHumanReadableErrorFromOnCallError(e, this.onCallApiUrl, 'install')
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (contextSrv.accessControlEnabled()) {
|
||||
return this.setupPluginError(
|
||||
'🚫 User is missing permission(s) ' +
|
||||
missingPermissions.join(', ') +
|
||||
' to setup OnCall before it can be used'
|
||||
);
|
||||
} else {
|
||||
return this.setupPluginError(
|
||||
'🚫 User with Admin permissions in your organization must sign on and setup OnCall before it can be used'
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const syncDataResponse = await PluginState.syncDataWithOnCall(this.onCallApiUrl);
|
||||
|
|
@ -223,13 +236,37 @@ export class RootBaseStore {
|
|||
this.appLoading = false;
|
||||
}
|
||||
|
||||
checkMissingSetupPermissions() {
|
||||
const fallback = contextSrv.user.orgRole === OrgRole.Admin && !contextSrv.accessControlEnabled();
|
||||
const setupRequiredPermissions = [
|
||||
'plugins:write',
|
||||
'org.users:read',
|
||||
'teams:read',
|
||||
'apikeys:create',
|
||||
'apikeys:delete',
|
||||
];
|
||||
return setupRequiredPermissions.filter(function (permission) {
|
||||
return !contextSrv.hasAccess(permission, fallback);
|
||||
});
|
||||
}
|
||||
|
||||
hasFeature(feature: string | AppFeature) {
|
||||
// todo use AppFeature only
|
||||
return this.features?.[feature];
|
||||
}
|
||||
|
||||
get license() {
|
||||
if (this.backendLicense) {
|
||||
return this.backendLicense;
|
||||
}
|
||||
if (CLOUD_VERSION_REGEX.test(APP_VERSION)) {
|
||||
return GRAFANA_LICENSE_CLOUD;
|
||||
}
|
||||
return GRAFANA_LICENSE_OSS;
|
||||
}
|
||||
|
||||
isOpenSource(): boolean {
|
||||
return this.backendLicense === GRAFANA_LICENSE_OSS;
|
||||
return this.license === GRAFANA_LICENSE_OSS;
|
||||
}
|
||||
|
||||
@observable
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
import { OrgRole } from '@grafana/data';
|
||||
import { contextSrv } from 'grafana/app/core/core';
|
||||
import { OnCallAppPluginMeta } from 'types';
|
||||
|
||||
import PluginState from 'state/plugin';
|
||||
import { UserActions, isUserActionAllowed as isUserActionAllowedOriginal } from 'utils/authorization';
|
||||
import { isUserActionAllowed as isUserActionAllowedOriginal } from 'utils/authorization';
|
||||
|
||||
import { RootBaseStore } from './';
|
||||
|
||||
jest.mock('state/plugin');
|
||||
jest.mock('utils/authorization');
|
||||
jest.mock('grafana/app/core/core', () => ({
|
||||
contextSrv: {
|
||||
user: {
|
||||
orgRole: null,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const isUserActionAllowed = isUserActionAllowedOriginal as jest.Mock<ReturnType<typeof isUserActionAllowedOriginal>>;
|
||||
|
||||
const PluginInstallAction = UserActions.PluginsInstall;
|
||||
|
||||
const generatePluginData = (
|
||||
onCallApiUrl: OnCallAppPluginMeta['jsonData']['onCallApiUrl'] = null
|
||||
): OnCallAppPluginMeta =>
|
||||
|
|
@ -42,7 +49,9 @@ describe('rootBaseStore', () => {
|
|||
const onCallApiUrl = 'http://asdfasdf.com';
|
||||
const rootBaseStore = new RootBaseStore();
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest.fn().mockResolvedValueOnce(null);
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: null });
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce(errorMsg);
|
||||
|
||||
// test
|
||||
|
|
@ -62,14 +71,16 @@ describe('rootBaseStore', () => {
|
|||
const rootBaseStore = new RootBaseStore();
|
||||
const maintenanceMessage = 'mncvnmvcmnvkjdjkd';
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest.fn().mockResolvedValueOnce(maintenanceMessage);
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: maintenanceMessage });
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
||||
// assertions
|
||||
expect(PluginState.checkIfBackendIsInMaintenanceMode).toHaveBeenCalledTimes(1);
|
||||
expect(PluginState.checkIfBackendIsInMaintenanceMode).toHaveBeenCalledWith();
|
||||
expect(PluginState.checkIfBackendIsInMaintenanceMode).toHaveBeenCalledWith(onCallApiUrl);
|
||||
|
||||
expect(rootBaseStore.appLoading).toBe(false);
|
||||
expect(rootBaseStore.initializationError).toEqual(`🚧 ${maintenanceMessage} 🚧`);
|
||||
|
|
@ -81,7 +92,9 @@ describe('rootBaseStore', () => {
|
|||
const onCallApiUrl = 'http://asdfasdf.com';
|
||||
const rootBaseStore = new RootBaseStore();
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest.fn().mockResolvedValueOnce(null);
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: null });
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce({
|
||||
is_user_anonymous: true,
|
||||
is_installed: true,
|
||||
|
|
@ -100,7 +113,7 @@ describe('rootBaseStore', () => {
|
|||
|
||||
expect(rootBaseStore.appLoading).toBe(false);
|
||||
expect(rootBaseStore.initializationError).toEqual(
|
||||
'😞 Unfortunately Grafana OnCall is available for authorized users only, please sign in to proceed.'
|
||||
'😞 Grafana OnCall is available for authorized users only, please sign in to proceed.'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -109,7 +122,9 @@ describe('rootBaseStore', () => {
|
|||
const onCallApiUrl = 'http://asdfasdf.com';
|
||||
const rootBaseStore = new RootBaseStore();
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest.fn().mockResolvedValueOnce(null);
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: null });
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce({
|
||||
is_user_anonymous: false,
|
||||
is_installed: false,
|
||||
|
|
@ -140,7 +155,13 @@ describe('rootBaseStore', () => {
|
|||
const onCallApiUrl = 'http://asdfasdf.com';
|
||||
const rootBaseStore = new RootBaseStore();
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest.fn().mockResolvedValueOnce(null);
|
||||
contextSrv.user.orgRole = OrgRole.Viewer;
|
||||
contextSrv.accessControlEnabled = jest.fn().mockReturnValue(false);
|
||||
contextSrv.hasAccess = jest.fn().mockReturnValue(false);
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: null });
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce({
|
||||
is_user_anonymous: false,
|
||||
is_installed: false,
|
||||
|
|
@ -159,14 +180,11 @@ describe('rootBaseStore', () => {
|
|||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledTimes(1);
|
||||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledWith(onCallApiUrl);
|
||||
|
||||
expect(isUserActionAllowed).toHaveBeenCalledTimes(1);
|
||||
expect(isUserActionAllowed).toHaveBeenCalledWith(PluginInstallAction);
|
||||
|
||||
expect(PluginState.installPlugin).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(rootBaseStore.appLoading).toBe(false);
|
||||
expect(rootBaseStore.initializationError).toEqual(
|
||||
'🚫 An Admin in your organization must sign on and setup OnCall before it can be used'
|
||||
'🚫 User with Admin permissions in your organization must sign on and setup OnCall before it can be used'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -179,7 +197,13 @@ describe('rootBaseStore', () => {
|
|||
const rootBaseStore = new RootBaseStore();
|
||||
const mockedLoadCurrentUser = jest.fn();
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest.fn().mockResolvedValueOnce(null);
|
||||
contextSrv.user.orgRole = OrgRole.Admin;
|
||||
contextSrv.accessControlEnabled = jest.fn().mockResolvedValueOnce(false);
|
||||
contextSrv.hasAccess = jest.fn().mockReturnValue(true);
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: null });
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce({
|
||||
...scenario,
|
||||
is_user_anonymous: false,
|
||||
|
|
@ -198,9 +222,6 @@ describe('rootBaseStore', () => {
|
|||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledTimes(1);
|
||||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledWith(onCallApiUrl);
|
||||
|
||||
expect(isUserActionAllowed).toHaveBeenCalledTimes(1);
|
||||
expect(isUserActionAllowed).toHaveBeenCalledWith(PluginInstallAction);
|
||||
|
||||
expect(PluginState.installPlugin).toHaveBeenCalledTimes(1);
|
||||
expect(PluginState.installPlugin).toHaveBeenCalledWith();
|
||||
|
||||
|
|
@ -211,6 +232,71 @@ describe('rootBaseStore', () => {
|
|||
expect(rootBaseStore.initializationError).toBeNull();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ role: OrgRole.Admin, missing_permissions: [], expected_result: true },
|
||||
{ role: OrgRole.Viewer, missing_permissions: [], expected_result: true },
|
||||
{
|
||||
role: OrgRole.Admin,
|
||||
missing_permissions: ['plugins:write', 'org.users:read', 'teams:read', 'apikeys:create', 'apikeys:delete'],
|
||||
expected_result: false,
|
||||
},
|
||||
{
|
||||
role: OrgRole.Viewer,
|
||||
missing_permissions: ['plugins:write', 'org.users:read', 'teams:read', 'apikeys:create', 'apikeys:delete'],
|
||||
expected_result: false,
|
||||
},
|
||||
])('signup is allowed, accessControlEnabled, various roles and permissions', async (scenario) => {
|
||||
// mocks/setup
|
||||
const onCallApiUrl = 'http://asdfasdf.com';
|
||||
const rootBaseStore = new RootBaseStore();
|
||||
const mockedLoadCurrentUser = jest.fn();
|
||||
|
||||
contextSrv.user.orgRole = scenario.role;
|
||||
contextSrv.accessControlEnabled = jest.fn().mockReturnValue(true);
|
||||
rootBaseStore.checkMissingSetupPermissions = jest.fn().mockImplementation(() => scenario.missing_permissions);
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: null });
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce({
|
||||
...scenario,
|
||||
is_user_anonymous: false,
|
||||
allow_signup: true,
|
||||
version: 'asdfasdf',
|
||||
license: 'asdfasdf',
|
||||
});
|
||||
isUserActionAllowed.mockReturnValueOnce(true);
|
||||
PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null);
|
||||
rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser;
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
||||
// assertions
|
||||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledTimes(1);
|
||||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledWith(onCallApiUrl);
|
||||
|
||||
expect(rootBaseStore.appLoading).toBe(false);
|
||||
|
||||
if (scenario.expected_result) {
|
||||
expect(PluginState.installPlugin).toHaveBeenCalledTimes(1);
|
||||
expect(PluginState.installPlugin).toHaveBeenCalledWith();
|
||||
|
||||
expect(mockedLoadCurrentUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockedLoadCurrentUser).toHaveBeenCalledWith();
|
||||
|
||||
expect(rootBaseStore.initializationError).toBeNull();
|
||||
} else {
|
||||
expect(PluginState.installPlugin).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(rootBaseStore.initializationError).toEqual(
|
||||
'🚫 User is missing permission(s) ' +
|
||||
scenario.missing_permissions.join(', ') +
|
||||
' to setup OnCall before it can be used'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('plugin is not installed, signup is allowed, the user is an admin, and plugin installation throws an error', async () => {
|
||||
// mocks/setup
|
||||
const onCallApiUrl = 'http://asdfasdf.com';
|
||||
|
|
@ -218,7 +304,13 @@ describe('rootBaseStore', () => {
|
|||
const installPluginError = new Error('asdasdfasdfasf');
|
||||
const humanReadableErrorMsg = 'asdfasldkfjaksdjflk';
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest.fn().mockResolvedValueOnce(null);
|
||||
contextSrv.user.orgRole = OrgRole.Admin;
|
||||
contextSrv.accessControlEnabled = jest.fn().mockReturnValue(false);
|
||||
contextSrv.hasAccess = jest.fn().mockReturnValue(true);
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: null });
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce({
|
||||
is_user_anonymous: false,
|
||||
is_installed: false,
|
||||
|
|
@ -238,9 +330,6 @@ describe('rootBaseStore', () => {
|
|||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledTimes(1);
|
||||
expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledWith(onCallApiUrl);
|
||||
|
||||
expect(isUserActionAllowed).toHaveBeenCalledTimes(1);
|
||||
expect(isUserActionAllowed).toHaveBeenCalledWith(PluginInstallAction);
|
||||
|
||||
expect(PluginState.installPlugin).toHaveBeenCalledTimes(1);
|
||||
expect(PluginState.installPlugin).toHaveBeenCalledWith();
|
||||
|
||||
|
|
@ -263,7 +352,9 @@ describe('rootBaseStore', () => {
|
|||
const version = 'asdfalkjslkjdf';
|
||||
const license = 'lkjdkjfdkjfdjkfd';
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest.fn().mockResolvedValueOnce(null);
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: null });
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce({
|
||||
is_user_anonymous: false,
|
||||
is_installed: true,
|
||||
|
|
@ -299,7 +390,9 @@ describe('rootBaseStore', () => {
|
|||
const mockedLoadCurrentUser = jest.fn();
|
||||
const syncDataWithOnCallError = 'asdasdfasdfasf';
|
||||
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest.fn().mockResolvedValueOnce(null);
|
||||
PluginState.checkIfBackendIsInMaintenanceMode = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ currently_undergoing_maintenance_message: null });
|
||||
PluginState.checkIfPluginIsConnected = jest.fn().mockResolvedValueOnce({
|
||||
is_user_anonymous: false,
|
||||
is_installed: true,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export enum Resource {
|
|||
OTHER_SETTINGS = 'other-settings',
|
||||
|
||||
TEAMS = 'teams',
|
||||
PLUGINS = 'plugins',
|
||||
}
|
||||
|
||||
export enum Action {
|
||||
|
|
@ -35,7 +34,6 @@ export enum Action {
|
|||
TEST = 'test',
|
||||
EXPORT = 'export',
|
||||
UPDATE_SETTINGS = 'update-settings',
|
||||
INSTALL = 'install',
|
||||
}
|
||||
|
||||
type Actions =
|
||||
|
|
@ -66,8 +64,7 @@ type Actions =
|
|||
| 'UserSettingsAdmin'
|
||||
| 'OtherSettingsRead'
|
||||
| 'OtherSettingsWrite'
|
||||
| 'TeamsWrite'
|
||||
| 'PluginsInstall';
|
||||
| 'TeamsWrite';
|
||||
|
||||
const roleMapping: Record<OrgRole, number> = {
|
||||
[OrgRole.Admin]: 0,
|
||||
|
|
@ -164,5 +161,4 @@ export const UserActions: { [action in Actions]: UserAction } = {
|
|||
|
||||
// These are not oncall specific
|
||||
TeamsWrite: constructAction(Resource.TEAMS, Action.WRITE, OrgRole.Admin, false),
|
||||
PluginsInstall: constructAction(Resource.PLUGINS, Action.INSTALL, OrgRole.Admin, false),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,9 +4,17 @@ import plugin from '../../package.json'; // eslint-disable-line
|
|||
export const APP_TITLE = 'Grafana OnCall';
|
||||
export const APP_SUBTITLE = `Developer-friendly incident response (${plugin?.version})`;
|
||||
|
||||
export const APP_VERSION = `${plugin?.version}`;
|
||||
|
||||
export const CLOUD_VERSION_REGEX = new RegExp('r[\\d]+-v[\\d]+.[\\d]+.[\\d]+');
|
||||
|
||||
// License
|
||||
export const GRAFANA_LICENSE_OSS = 'OpenSource';
|
||||
|
||||
export const GRAFANA_LICENSE_CLOUD = 'Cloud';
|
||||
|
||||
export const FALLBACK_LICENSE = CLOUD_VERSION_REGEX.test(APP_VERSION) ? GRAFANA_LICENSE_CLOUD : GRAFANA_LICENSE_OSS;
|
||||
|
||||
// height of new Grafana sticky header with breadcrumbs
|
||||
export const GRAFANA_HEADER_HEIGTH = 80;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue