diff --git a/.github/actions/build-sign-and-package-plugin/action.yml b/.github/actions/build-sign-and-package-plugin/action.yml index 29c00be816..b4a6321f05 100644 --- a/.github/actions/build-sign-and-package-plugin/action.yml +++ b/.github/actions/build-sign-and-package-plugin/action.yml @@ -2,18 +2,10 @@ name: Build, sign, and package plugin description: Build, sign, and package plugin inputs: plugin_version_number: - description: "The version number of the plugin" + description: | + The version number of the plugin. NOTE: this action will chop off the leading "v" to use + it as the official plugin version. required: true - grafana_access_policy_token: - description: "The Grafana access policy token used to sign the plugin" - required: true - working_directory: - description: "The working directory of the plugin" - required: true - is_enterprise: - description: "Whether the plugin is an enterprise build or not" - required: false - default: "false" outputs: artifact_filename: description: "The filename of the plugin artifact" @@ -21,12 +13,24 @@ outputs: runs: using: "composite" steps: + # This will fetch the secret keys from vault and set them as environment variables for subsequent steps + - name: Get Vault secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@main + with: + repo_secrets: | + GRAFANA_ACCESS_POLICY_TOKEN=github_actions:cloud-access-policy-token + - name: Determine official plugin version + id: plugin-version + shell: bash + run: | + # VERY IMPORTANT: chop off the "v".. this tells the oncall plugin that this is an OSS build + PLUGIN_VERSION="$(echo ${{ inputs.plugin_version_number }} | sed 's/^v//')" + echo version="$PLUGIN_VERSION" >> $GITHUB_OUTPUT - name: Determine artifact filename shell: bash id: artifact-filename - # yamllint disable rule:line-length run: | - echo filename="grafana-oncall${{ inputs.is_enterprise == 'true' && '-ee' || '' }}-app-${{ inputs.plugin_version_number }}.zip" >> $GITHUB_OUTPUT + echo filename="grafana-oncall-app-${{ steps.plugin-version.outputs.version }}.zip" >> $GITHUB_OUTPUT - name: Install Go uses: actions/setup-go@v4 with: @@ -36,16 +40,18 @@ runs: run: go install github.com/magefile/mage@v1.15.0 - name: Build, sign, and package plugin shell: bash - working-directory: ${{ inputs.working_directory }} - env: - GRAFANA_ACCESS_POLICY_TOKEN: ${{ inputs.grafana_access_policy_token }} + working-directory: grafana-plugin run: | - jq --arg v "${{ inputs.plugin_version_number }}" '.version=$v' package.json > package.new && mv package.new package.json && jq '.version' package.json; + jq --arg v "${{ steps.plugin-version.outputs.version }}" '.version=$v' package.json > package.new && \ + mv package.new package.json && \ + jq '.version' package.json; + pnpm build mage buildAll || true + pnpm sign + if [ ! -f dist/MANIFEST.txt ]; then echo "Sign failed, MANIFEST.txt not created, aborting." && exit 1; fi mv dist grafana-oncall-app zip -r grafana-oncall-app.zip ./grafana-oncall-app cp grafana-oncall-app.zip ${{ steps.artifact-filename.outputs.filename }} - # yamllint enable rule:line-length diff --git a/.github/workflows/on-release-published.yml b/.github/workflows/on-release-published.yml index e662d995f5..82c40e8d77 100644 --- a/.github/workflows/on-release-published.yml +++ b/.github/workflows/on-release-published.yml @@ -36,15 +36,12 @@ jobs: uses: grafana/shared-workflows/actions/get-vault-secrets@main with: repo_secrets: | - GRAFANA_ACCESS_POLICY_TOKEN=github_actions:cloud-access-policy-token GCS_PLUGIN_PUBLISHER_SERVICE_ACCOUNT_JSON=github_actions:gcs-plugin-publisher - name: Build, sign, and package plugin id: build-sign-and-package-plugin uses: ./.github/actions/build-sign-and-package-plugin with: plugin_version_number: ${{ github.ref_name }} - grafana_access_policy_token: ${{ env.GRAFANA_ACCESS_POLICY_TOKEN }} - working_directory: grafana-plugin - name: Authenticate with GCS uses: google-github-actions/auth@v2 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 85f704b5ee..184f1228a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,20 +64,20 @@ repos: - "@grafana/eslint-config@^5.0.0" - eslint-plugin-promise@^6.1.1 - - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v2.7.1" - hooks: - - id: prettier - name: prettier - types_or: [css, javascript, jsx, ts, tsx] - files: ^grafana-plugin/src - additional_dependencies: - - prettier@2.8.2 - - id: prettier - name: prettier - json - types_or: [json] - additional_dependencies: - - prettier@2.8.2 + # - repo: https://github.com/pre-commit/mirrors-prettier + # rev: "v2.7.1" + # hooks: + # - id: prettier + # name: prettier + # types_or: [css, javascript, jsx, ts, tsx] + # files: ^grafana-plugin/src + # additional_dependencies: + # - prettier@2.8.2 + # - id: prettier + # name: prettier - json + # types_or: [json] + # additional_dependencies: + # - prettier@2.8.2 - repo: https://github.com/thibaudcolas/pre-commit-stylelint rev: v13.13.1 diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index bb8d6f660d..9b9d007fd5 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -130,6 +130,38 @@ curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unresolve" \ `POST {{API_URL}}/api/v1/alert_groups//unresolve` +## Silence an alert group + +```shell +curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/silence" \ + --request POST \ + --header "Authorization: meowmeowmeow" \ + --header "Content-Type: application/json" \ + --data '{ + "delay": 10800 + }' +``` + +**HTTP request** + +`POST {{API_URL}}/api/v1/alert_groups//silence` + +| Parameter | Required | Description | +|-----------|:--------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `delay` | Yes | The duration of silence in seconds, `-1` for silencing the alert forever | + +## Unsilence an alert group + +```shell +curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unsilence" \ + --request POST \ + --header "Authorization: meowmeowmeow" +``` + +**HTTP request** + +`POST {{API_URL}}/api/v1/alert_groups//unsilence` + ## Delete an alert group ```shell diff --git a/engine/apps/public_api/tests/test_alert_groups.py b/engine/apps/public_api/tests/test_alert_groups.py index 3cfc9c8537..758b0d9924 100644 --- a/engine/apps/public_api/tests/test_alert_groups.py +++ b/engine/apps/public_api/tests/test_alert_groups.py @@ -635,3 +635,98 @@ def test_alert_group_unresolve( alert_group.refresh_from_db() assert alert_group.resolved is False assert alert_group.log_records.last().action_source == ActionSource.API + + +@pytest.mark.parametrize( + "acknowledged,resolved,attached,status_code,data,response_msg", + [ + (False, False, False, status.HTTP_200_OK, {"delay": 60}, None), + (False, False, False, status.HTTP_400_BAD_REQUEST, {"delay": -2}, "invalid delay value"), + (False, False, False, status.HTTP_400_BAD_REQUEST, {"delay": "fuzz"}, "invalid delay value"), + (False, False, False, status.HTTP_400_BAD_REQUEST, {}, "delay is required"), + (True, False, False, status.HTTP_400_BAD_REQUEST, {"delay": 60}, "Can't silence an acknowledged alert group"), + (False, True, False, status.HTTP_400_BAD_REQUEST, {"delay": 60}, "Can't silence a resolved alert group"), + (False, False, True, status.HTTP_400_BAD_REQUEST, {"delay": 60}, "Can't silence an attached alert group"), + ], +) +@pytest.mark.django_db +def test_alert_group_silence( + make_organization_and_user_with_token, + make_alert_receive_channel, + make_alert_group, + acknowledged, + resolved, + attached, + status_code, + data, + response_msg, +): + organization, _, token = make_organization_and_user_with_token() + alert_receive_channel = make_alert_receive_channel(organization) + root_alert_group = make_alert_group(alert_receive_channel) + alert_group = make_alert_group( + alert_receive_channel, + acknowledged=acknowledged, + resolved=resolved, + root_alert_group=root_alert_group if attached else None, + ) + + client = APIClient() + url = reverse("api-public:alert_groups-silence", kwargs={"pk": alert_group.public_primary_key}) + response = client.post(url, data=data, HTTP_AUTHORIZATION=token) + + if status_code == status.HTTP_200_OK: + alert_group.refresh_from_db() + assert alert_group.silenced is True + assert alert_group.log_records.last().action_source == ActionSource.API + else: + assert alert_group.silenced is False + assert response.status_code == status_code + assert response_msg == response.json()["detail"] + + +@pytest.mark.parametrize( + "silenced,resolved,acknowledged,attached,status_code,response_msg", + [ + (True, False, False, False, status.HTTP_200_OK, None), + (False, False, False, False, status.HTTP_400_BAD_REQUEST, "Can't unsilence an unsilenced alert group"), + (True, True, False, False, status.HTTP_400_BAD_REQUEST, "Can't unsilence a resolved alert group"), + (True, False, True, False, status.HTTP_400_BAD_REQUEST, "Can't unsilence an acknowledged alert group"), + (True, False, False, True, status.HTTP_400_BAD_REQUEST, "Can't unsilence an attached alert group"), + ], +) +@pytest.mark.django_db +def test_alert_group_unsilence( + make_organization_and_user_with_token, + make_alert_receive_channel, + make_alert_group, + silenced, + resolved, + acknowledged, + attached, + status_code, + response_msg, +): + organization, _, token = make_organization_and_user_with_token() + alert_receive_channel = make_alert_receive_channel(organization) + root_alert_group = make_alert_group(alert_receive_channel) + alert_group = make_alert_group( + alert_receive_channel, + acknowledged=acknowledged, + resolved=resolved, + silenced=silenced, + root_alert_group=root_alert_group if attached else None, + ) + + client = APIClient() + url = reverse("api-public:alert_groups-unsilence", kwargs={"pk": alert_group.public_primary_key}) + response = client.post(url, HTTP_AUTHORIZATION=token) + + if status_code == status.HTTP_200_OK: + alert_group.refresh_from_db() + assert alert_group.silenced is False + assert alert_group.log_records.last().action_source == ActionSource.API + else: + assert alert_group.silenced == silenced + assert response.status_code == status_code + assert response_msg == response.json()["detail"] diff --git a/engine/apps/public_api/views/alert_groups.py b/engine/apps/public_api/views/alert_groups.py index 0db6f34932..c1b5fe1d54 100644 --- a/engine/apps/public_api/views/alert_groups.py +++ b/engine/apps/public_api/views/alert_groups.py @@ -213,3 +213,49 @@ def unresolve(self, request, pk): alert_group.un_resolve_by_user_or_backsync(self.request.user, action_source=ActionSource.API) return Response(status=status.HTTP_200_OK) + + @action(methods=["post"], detail=True) + def silence(self, request, pk=None): + alert_group = self.get_object() + + delay = request.data.get("delay") + if delay is None: + raise BadRequest(detail="delay is required") + try: + delay = int(delay) + except ValueError: + raise BadRequest(detail="invalid delay value") + if delay < -1: + raise BadRequest(detail="invalid delay value") + + if alert_group.resolved: + raise BadRequest(detail="Can't silence a resolved alert group") + + if alert_group.acknowledged: + raise BadRequest(detail="Can't silence an acknowledged alert group") + + if alert_group.root_alert_group is not None: + raise BadRequest(detail="Can't silence an attached alert group") + + alert_group.silence_by_user_or_backsync(request.user, silence_delay=delay, action_source=ActionSource.API) + return Response(status=status.HTTP_200_OK) + + @action(methods=["post"], detail=True) + def unsilence(self, request, pk=None): + alert_group = self.get_object() + + if not alert_group.silenced: + raise BadRequest(detail="Can't unsilence an unsilenced alert group") + + if alert_group.resolved: + raise BadRequest(detail="Can't unsilence a resolved alert group") + + if alert_group.acknowledged: + raise BadRequest(detail="Can't unsilence an acknowledged alert group") + + if alert_group.root_alert_group is not None: + raise BadRequest(detail="Can't unsilence an attached alert group") + + alert_group.un_silence_by_user_or_backsync(request.user, action_source=ActionSource.API) + + return Response(status=status.HTTP_200_OK) diff --git a/grafana-plugin/e2e-tests/schedules/addOverride.test.ts b/grafana-plugin/e2e-tests/schedules/addOverride.test.ts index 41fccad50d..e11b0614a7 100644 --- a/grafana-plugin/e2e-tests/schedules/addOverride.test.ts +++ b/grafana-plugin/e2e-tests/schedules/addOverride.test.ts @@ -1,7 +1,7 @@ import dayjs from 'dayjs'; import { test, expect, Locator } from '../fixtures'; -import { MOSCOW_TIMEZONE } from '../utils/constants'; +import { isGrafanaVersionGreaterThan, MOSCOW_TIMEZONE } from '../utils/constants'; import { clickButton, generateRandomValue } from '../utils/forms'; import { setTimezoneInProfile } from '../utils/grafanaProfile'; import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule'; @@ -12,6 +12,12 @@ test('Default dates in override creation modal are set to today', async ({ admin const onCallScheduleName = generateRandomValue(); await createOnCallSchedule(page, onCallScheduleName, userName); + await page.clock.setFixedTime(new Date().setHours(12, 0, 0, 0)); + await page.getByTestId('timezone-select').locator('svg').click(); + await (isGrafanaVersionGreaterThan('11.0.0') ? page.getByRole('option') : page.getByLabel('Select option')) + .getByText(/^GMT$/) + .click(); + await clickButton({ page, buttonText: 'Add override' }); const overrideFormDateInputs = await getOverrideFormDateInputs(page); diff --git a/grafana-plugin/e2e-tests/schedules/timezones.test.ts b/grafana-plugin/e2e-tests/schedules/timezones.test.ts index e20b77b0d3..ab7d7fbd03 100644 --- a/grafana-plugin/e2e-tests/schedules/timezones.test.ts +++ b/grafana-plugin/e2e-tests/schedules/timezones.test.ts @@ -22,7 +22,7 @@ test.skip('dates in schedule are correct according to selected current timezone' * Always set a fixed time of today's date but at 12:00:00 (noon) * * This solves the issue here https://github.com/grafana/oncall/issues/4991 - * where we would occasionally see this test flake if it srtated and finished at a different hour + * where we would occasionally see this test flake if it started and finished at a different hour * * See playwright docs for more details * https://playwright.dev/docs/clock diff --git a/grafana-plugin/e2e-tests/utils/userSettings.ts b/grafana-plugin/e2e-tests/utils/userSettings.ts index 6698899c18..3d16b44582 100644 --- a/grafana-plugin/e2e-tests/utils/userSettings.ts +++ b/grafana-plugin/e2e-tests/utils/userSettings.ts @@ -70,7 +70,7 @@ export const configureUserNotificationSettings = async (page: Page, notifyBy: No // select our notification type, first check if we have any already defined, if so, click the // "Add Notification Step" button const defaultNotificationSettingsSection = getDefaultNotificationSettingsSectionByTestId(page); - const addNotificationStepText = 'Add Notification Step'; + const addNotificationStepText = 'Add notification step'; if (!(await defaultNotificationSettingsSection.locator(`button >> text=${addNotificationStepText}`).isVisible())) { await clickButton({ diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 7a587b111f..5fef73c9fe 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -1,6 +1,6 @@ { "name": "grafana-oncall-app", - "version": "1.0.0", + "version": "1.9.23", "description": "Grafana OnCall Plugin", "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx --max-warnings=20 ./src ./e2e-tests", @@ -11,9 +11,8 @@ "build:dev": "NODE_ENV=development webpack -c ./webpack.config.ts --env development", "labels:link": "pnpm --dir ../../gops-labels/frontend link && pnpm link \"@grafana/labels\" && pnpm --dir ../../gops-labels/frontend watch", "labels:unlink": "pnpm --dir ../../gops-labels/frontend unlink", - "mage:build-dev": "go mod download && mage -v build:debug", - "mage:watch": "go mod download && mage -v watch", - "mod:download": "go mod download", + "mage:build-dev": "mage -v build:debug", + "mage:watch": "mage -v watch", "test-utc": "TZ=UTC jest --verbose --testNamePattern '^((?!@london-tz).)*$'", "test-london-tz": "TZ=Europe/London jest --verbose --testNamePattern '@london-tz'", "test": "PLUGIN_ID=grafana-oncall-app pnpm test-utc && pnpm test-london-tz", diff --git a/grafana-plugin/pkg/plugin/settings.go b/grafana-plugin/pkg/plugin/settings.go index 8386c7dbee..944bd96f1c 100644 --- a/grafana-plugin/pkg/plugin/settings.go +++ b/grafana-plugin/pkg/plugin/settings.go @@ -105,8 +105,8 @@ type OnCallSettingsCache struct { otherPluginSettingsExpiry time.Time } -const CLOUD_VERSION_PATTERN = `^(r\d+-v?\d+\.\d+\.\d+|^github-actions-\d+)$` -const OSS_VERSION_PATTERN = `^(v?\d+\.\d+\.\d+|dev-oss)$` +const CLOUD_VERSION_PATTERN = `^(v\d+\.\d+\.\d+|github-actions-[a-zA-Z0-9-]+)$` +const OSS_VERSION_PATTERN = `^(\d+\.\d+\.\d+)$` const CLOUD_LICENSE_NAME = "Cloud" const OPEN_SOURCE_LICENSE_NAME = "OpenSource" const INCIDENT_PLUGIN_ID = "grafana-incident-app" diff --git a/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/NotificationPoliciesSelect.tsx b/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/NotificationPoliciesSelect.tsx index c5fd989014..e0e2773671 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/NotificationPoliciesSelect.tsx +++ b/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/NotificationPoliciesSelect.tsx @@ -24,12 +24,12 @@ export const NotificationPoliciesSelect: FC = ({ disabled = false, import { value: NotificationPolicyValue.Default, label: 'Default', - description: 'Use "Default notifications" from users personal settings', + description: 'Use "Default notification rules" from users personal settings', }, { value: NotificationPolicyValue.Important, label: 'Important', - description: 'Use "Important notifications" from users personal settings', + description: 'Use "Important notification rules" from users personal settings', }, ]} onChange={onChange} diff --git a/grafana-plugin/src/containers/Alerts/Alerts.tsx b/grafana-plugin/src/containers/Alerts/Alerts.tsx index 6a5b3d70a3..d08b1df348 100644 --- a/grafana-plugin/src/containers/Alerts/Alerts.tsx +++ b/grafana-plugin/src/containers/Alerts/Alerts.tsx @@ -19,8 +19,6 @@ import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers'; import { AppFeature } from 'state/features'; import { useStore } from 'state/useStore'; -import plugin from '../../../package.json'; // eslint-disable-line - enum AlertID { CONNECTIVITY_WARNING = 'Connectivity Warning', USER_GOOGLE_OAUTH2_TOKEN_MISSING_SCOPES = 'User Google OAuth2 token is missing scopes', @@ -59,7 +57,6 @@ export const Alerts = observer(() => { organizationStore: { currentOrganization }, } = store; - const versionMismatchLocalStorageId = `version_mismatch_${store.backendVersion}_${plugin?.version}`; const isChatOpsConnected = getIfChatOpsConnected(currentUser); const isPhoneVerified = currentUser?.cloud_connection_status === 3 || currentUser?.verified_phone_number; @@ -70,7 +67,6 @@ export const Alerts = observer(() => { !showSlackInstallAlert && !showCurrentUserGoogleOAuth2TokenIsMissingScopes() && !showBannerTeam() && - !showMismatchWarning() && !showChannelWarnings() ) { return null; @@ -117,30 +113,6 @@ export const Alerts = observer(() => { /> )} - {showMismatchWarning() && ( - - Please make sure you have the same versions of the Grafana OnCall plugin and the Grafana OnCall engine, - otherwise there could be issues with your Grafana OnCall installation! -
- {`Current plugin version: ${plugin.version}, current engine version: ${store.backendVersion}`} -
- Please see{' '} - - the update instructions - - . -
- )} {showChannelWarnings() && ( { return Boolean(currentOrganization?.banner?.title) && !getItem(currentOrganization?.banner?.title); } - function showMismatchWarning(): boolean { - return ( - store.isOpenSource && - store.backendVersion && - plugin?.version && - store.backendVersion !== plugin?.version && - !getItem(versionMismatchLocalStorageId) - ); - } - function showChannelWarnings(): boolean { return Boolean( currentOrganization && diff --git a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx index b7282a1ad0..8ea4ec6972 100644 --- a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx +++ b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx @@ -162,7 +162,7 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) => return ( +
Edit {template.displayName} template @@ -188,26 +188,28 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) => closeOnMaskClick={false} width={'95%'} > -
- - {renderCheatSheet()} - +
+
+ + {renderCheatSheet()} + +
); diff --git a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx index 7090c8666f..783c373aaf 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx +++ b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { css, cx } from '@emotion/css'; import { Button, Icon, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui'; import { UserActions } from 'helpers/authorization/authorization'; -import { StackSize } from 'helpers/consts'; +import { IS_CURRENT_ENV_CLOUD, StackSize } from 'helpers/consts'; import { isMobile, openNotification, openWarningNotification, openErrorNotification } from 'helpers/helpers'; import { useInitializePlugin } from 'helpers/hooks'; import { observer } from 'mobx-react'; @@ -140,7 +140,7 @@ export const MobileAppConnection = observer(({ userPk }: Props) => { // Show link to cloud page for OSS instances with no cloud connection if ( - store.isOpenSource && + !IS_CURRENT_ENV_CLOUD && store.hasFeature(AppFeature.CloudConnection) && !cloudStore.cloudConnectionStatus.cloud_connection_status ) { @@ -191,7 +191,7 @@ export const MobileAppConnection = observer(({ userPk }: Props) => { {isQRBlurry && }
- {store.isOpenSource && QRCodeDataParsed && ( + {!IS_CURRENT_ENV_CLOUD && QRCodeDataParsed && ( Server URL embedded in this QR:
diff --git a/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx b/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx index 54b65010d9..20485d301d 100644 --- a/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx +++ b/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx @@ -64,7 +64,7 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio const title = ( - {isImportant ? 'Important Notifications' : 'Default Notifications'} + {isImportant ? 'Important notification rules' : 'Default notification rules'}
diff --git a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx index 116e4b2230..d032250e19 100644 --- a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx +++ b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx @@ -8,11 +8,12 @@ import { DEFAULT_PAGE, DOCS_ONCALL_OSS_INSTALL, DOCS_SERVICE_ACCOUNTS, + IS_CURRENT_ENV_CLOUD, PLUGIN_CONFIG, PLUGIN_ROOT, REQUEST_HELP_URL, } from 'helpers/consts'; -import { getIsExternalServiceAccountFeatureAvailable, getIsRunningOpenSourceVersion } from 'helpers/helpers'; +import { getIsExternalServiceAccountFeatureAvailable } from 'helpers/helpers'; import { useOnMount } from 'helpers/hooks'; import { validateURL } from 'helpers/string'; import { observer } from 'mobx-react'; @@ -50,7 +51,7 @@ export const PluginConfigPage = observer((props: PluginConfigPageProps Configure Grafana OnCall - {getIsRunningOpenSourceVersion() ? : } + {IS_CURRENT_ENV_CLOUD ? : } ); }); diff --git a/grafana-plugin/src/containers/PluginInitializer/PluginInitializer.tsx b/grafana-plugin/src/containers/PluginInitializer/PluginInitializer.tsx index ffdb6b3bbf..a76d14e5b2 100644 --- a/grafana-plugin/src/containers/PluginInitializer/PluginInitializer.tsx +++ b/grafana-plugin/src/containers/PluginInitializer/PluginInitializer.tsx @@ -1,8 +1,7 @@ import React, { FC } from 'react'; import { Button, Stack, LoadingPlaceholder } from '@grafana/ui'; -import { REQUEST_HELP_URL, PLUGIN_CONFIG } from 'helpers/consts'; -import { getIsRunningOpenSourceVersion } from 'helpers/helpers'; +import { REQUEST_HELP_URL, PLUGIN_CONFIG, IS_CURRENT_ENV_CLOUD } from 'helpers/consts'; import { useInitializePlugin } from 'helpers/hooks'; import { observer } from 'mobx-react'; import { useNavigate } from 'react-router-dom-v5-compat'; @@ -34,12 +33,11 @@ export const PluginInitializer: FC = observer(({ childre }); const PluginNotConnectedFullPageError = observer(() => { - const isOpenSource = getIsRunningOpenSourceVersion(); const isCurrentUserAdmin = window.grafanaBootData.user.orgRole === 'Admin'; const navigate = useNavigate(); const getSubtitleExtension = () => { - if (!isOpenSource) { + if (IS_CURRENT_ENV_CLOUD) { return 'request help from our support team.'; } return isCurrentUserAdmin @@ -61,8 +59,8 @@ const PluginNotConnectedFullPageError = observer(() => { - {!isOpenSource && } - {isOpenSource && isCurrentUserAdmin && ( + {IS_CURRENT_ENV_CLOUD && } + {!IS_CURRENT_ENV_CLOUD && isCurrentUserAdmin && ( )} diff --git a/grafana-plugin/src/helpers/consts.ts b/grafana-plugin/src/helpers/consts.ts index d11503e097..5d8f034911 100644 --- a/grafana-plugin/src/helpers/consts.ts +++ b/grafana-plugin/src/helpers/consts.ts @@ -9,13 +9,22 @@ export const PluginId = { } as const; export type PluginId = (typeof PluginId)[keyof typeof PluginId]; -export const getIsDevelopmentEnv = () => { +// Determine current environment: cloud, oss or local +const CLOUD_VERSION_REGEX = /^(v\d+\.\d+\.\d+|github-actions-[a-zA-Z0-9-]+)$/ +const determineCurrentEnv = (): 'oss' | 'cloud' | 'local' => { + if (CLOUD_VERSION_REGEX.test(plugin?.version)) { + return 'cloud'; + } try { - return process.env.NODE_ENV === 'development'; + return process.env.NODE_ENV === 'development' ? 'local' : 'oss'; } catch (error) { - return false; + return 'cloud'; } }; +const CURRENT_ENV = determineCurrentEnv(); +export const IS_CURRENT_ENV_CLOUD = CURRENT_ENV === 'cloud'; +export const IS_CURRENT_ENV_OSS = CURRENT_ENV === 'oss'; +export const IS_CURRENT_ENV_LOCAL = CURRENT_ENV === 'local'; export const getPluginId = (): PluginId => { try { @@ -28,17 +37,6 @@ export const getPluginId = (): PluginId => { // Navbar 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]+|github-actions-[\\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_HEIGHT = 80; diff --git a/grafana-plugin/src/helpers/faro.test.tsx b/grafana-plugin/src/helpers/faro.test.tsx index f9e8a56e9d..81d85fbbdd 100644 --- a/grafana-plugin/src/helpers/faro.test.tsx +++ b/grafana-plugin/src/helpers/faro.test.tsx @@ -22,6 +22,12 @@ jest.mock('@grafana/faro-web-tracing', () => ({ TracingInstrumentation: jest.fn(), })); +jest.mock('./consts', () => ({ + __esModule: true, + ...jest.requireActual('./consts'), + IS_CURRENT_ENV_CLOUD: true, +})); + describe('Faro', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/grafana-plugin/src/helpers/faro.ts b/grafana-plugin/src/helpers/faro.ts index 05572eca9d..581f07b0ad 100644 --- a/grafana-plugin/src/helpers/faro.ts +++ b/grafana-plugin/src/helpers/faro.ts @@ -6,10 +6,10 @@ import { FARO_ENDPOINT_DEV, FARO_ENDPOINT_OPS, FARO_ENDPOINT_PROD, + IS_CURRENT_ENV_CLOUD, ONCALL_DEV, ONCALL_OPS, ONCALL_PROD, - getIsDevelopmentEnv, getPluginId, } from './consts'; import { safeJSONStringify } from './string'; @@ -33,7 +33,7 @@ class BaseFaroHelper { faro: Faro; initializeFaro(onCallApiUrl: string) { - if (this.faro || getIsDevelopmentEnv()) { + if (this.faro || !IS_CURRENT_ENV_CLOUD) { return undefined; } diff --git a/grafana-plugin/src/helpers/helpers.ts b/grafana-plugin/src/helpers/helpers.ts index 30b0a6047f..a8dad29970 100644 --- a/grafana-plugin/src/helpers/helpers.ts +++ b/grafana-plugin/src/helpers/helpers.ts @@ -8,8 +8,6 @@ import { isArray, concat, every, isEmpty, isObject, isPlainObject, flatMap, map, import { isNetworkError } from 'network/network'; -import { CLOUD_VERSION_REGEX, getPluginId } from './consts'; - export class KeyValuePair { key: T; value: string; @@ -153,8 +151,6 @@ export const isCurrentGrafanaVersionEqualOrGreaterThan = ({ ); }; -export const getIsRunningOpenSourceVersion = () => !CLOUD_VERSION_REGEX.test(config.apps[getPluginId()]?.version); - export const getIsExternalServiceAccountFeatureAvailable = () => isCurrentGrafanaVersionEqualOrGreaterThan({ minMajor: 10, minMinor: 3 }) && config.featureToggles.externalServiceAccounts; diff --git a/grafana-plugin/src/navbar/Header/Header.tsx b/grafana-plugin/src/navbar/Header/Header.tsx index d69837c8d6..c5485d9d04 100644 --- a/grafana-plugin/src/navbar/Header/Header.tsx +++ b/grafana-plugin/src/navbar/Header/Header.tsx @@ -2,19 +2,17 @@ import React from 'react'; import { cx } from '@emotion/css'; import { Card, Stack, useStyles2 } from '@grafana/ui'; -import { APP_SUBTITLE } from 'helpers/consts'; +import { APP_SUBTITLE, IS_CURRENT_ENV_OSS } from 'helpers/consts'; import { observer } from 'mobx-react'; import gitHubStarSVG from 'assets/img/github_star.svg'; import logo from 'assets/img/logo.svg'; import { Alerts } from 'containers/Alerts/Alerts'; import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers'; -import { useStore } from 'state/useStore'; import { getHeaderStyles } from './Header.styles'; export const Header = observer(() => { - const store = useStore(); const styles = useStyles2(getHeaderStyles); return ( @@ -34,7 +32,7 @@ export const Header = observer(() => { ); function renderHeading() { - if (store.isOpenSource) { + if (IS_CURRENT_ENV_OSS) { return (

Grafana OnCall

diff --git a/grafana-plugin/src/pages/insights/Insights.tsx b/grafana-plugin/src/pages/insights/Insights.tsx index 597bc2e7f6..d02d81c6bf 100644 --- a/grafana-plugin/src/pages/insights/Insights.tsx +++ b/grafana-plugin/src/pages/insights/Insights.tsx @@ -16,7 +16,7 @@ import { useSceneApp, } from '@grafana/scenes'; import { Alert, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui'; -import { DOCS_ROOT, StackSize, PLUGIN_ROOT } from 'helpers/consts'; +import { DOCS_ROOT, StackSize, PLUGIN_ROOT, IS_CURRENT_ENV_OSS } from 'helpers/consts'; import { observer } from 'mobx-react'; import { Text } from 'components/Text/Text'; @@ -40,7 +40,6 @@ import getVariables from './variables'; export const Insights = observer(() => { const { - isOpenSource, insightsDatasource, organizationStore: { currentOrganization }, } = useStore(); @@ -49,11 +48,11 @@ export const Insights = observer(() => { const config = useMemo( () => ({ - isOpenSource, - datasource: { uid: isOpenSource ? '$datasource' : insightsDatasource }, + isOpenSource: IS_CURRENT_ENV_OSS, + datasource: { uid: IS_CURRENT_ENV_OSS ? '$datasource' : insightsDatasource }, stack: currentOrganization?.stack_slug, }), - [isOpenSource, currentOrganization?.stack_slug] + [currentOrganization?.stack_slug] ); const variables = useMemo(() => getVariables(config), [config]); @@ -69,7 +68,7 @@ export const Insights = observer(() => { return undefined; } const dataSourceListener = - isOpenSource && + IS_CURRENT_ENV_OSS && variables.datasource.subscribeToState(({ text }) => { setDatasource(`${text}`); }); @@ -86,7 +85,7 @@ export const Insights = observer(() => { {isAnyAlertCreatedMoreThan20SecsAgo ? ( <> - {isOpenSource && !datasource && } + {IS_CURRENT_ENV_OSS && !datasource && } ) : ( diff --git a/grafana-plugin/src/pages/settings/tabs/ChatOps/ChatOps.tsx b/grafana-plugin/src/pages/settings/tabs/ChatOps/ChatOps.tsx index cdc0ec21b9..ea768c1eff 100644 --- a/grafana-plugin/src/pages/settings/tabs/ChatOps/ChatOps.tsx +++ b/grafana-plugin/src/pages/settings/tabs/ChatOps/ChatOps.tsx @@ -4,6 +4,7 @@ import { css } from '@emotion/css'; import { AppRootProps, GrafanaTheme2 } from '@grafana/data'; import { Alert, Icon, Stack, Themeable2, withTheme2 } from '@grafana/ui'; import { LocationHelper } from 'helpers/LocationHelper'; +import { IS_CURRENT_ENV_OSS } from 'helpers/consts'; import { observer } from 'mobx-react'; import { VerticalTabsBar, VerticalTab } from 'components/VerticalTabsBar/VerticalTabsBar'; @@ -46,10 +47,10 @@ export class _ChatOpsPage extends React.Component { render() { const { activeTab } = this.state; - const { store, theme } = this.props; + const { theme } = this.props; const styles = getStyles(theme); - if (!this.isChatOpsConfigured() && store.isOpenSource) { + if (!this.isChatOpsConfigured() && IS_CURRENT_ENV_OSS) { return this.renderNoChatOpsBannerInfo(); } diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index 14e22b044d..ca9216a518 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -352,11 +352,11 @@ class Users extends React.Component { // Show warnining if no notifications are set if (!this.renderNotificationsChain(user)) { - warnings.push('No Default Notifications'); + warnings.push('No default notification rules'); } if (!this.renderImportantNotificationsChain(user)) { - warnings.push('No Important Notifications'); + warnings.push('No important notification rules'); } let phone_verified = user.verified_phone_number !== null; @@ -428,13 +428,13 @@ class Users extends React.Component { }, { width: '20%', - title: 'Default Notifications', + title: 'Default notification rules', key: 'notifications-chain', render: this.renderNotificationsChain, }, { width: '20%', - title: 'Important Notifications', + title: 'Important notification rules', key: 'important-notifications-chain', render: this.renderImportantNotificationsChain, }, diff --git a/grafana-plugin/src/state/rootBaseStore/RootBaseStore.ts b/grafana-plugin/src/state/rootBaseStore/RootBaseStore.ts index 8087436c3f..c23080828f 100644 --- a/grafana-plugin/src/state/rootBaseStore/RootBaseStore.ts +++ b/grafana-plugin/src/state/rootBaseStore/RootBaseStore.ts @@ -1,8 +1,7 @@ import { OnCallAppPluginMeta } from 'app-types'; import { retryFailingPromises } from 'helpers/async'; -import { APP_VERSION, CLOUD_VERSION_REGEX, GRAFANA_LICENSE_CLOUD, GRAFANA_LICENSE_OSS } from 'helpers/consts'; import { loadJs } from 'helpers/loadJs'; -import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { action, makeObservable, observable, runInAction } from 'mobx'; import qs from 'query-string'; import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel'; @@ -43,12 +42,6 @@ export class RootBaseStore { @observable isBasicDataLoaded = false; - @observable - backendVersion = ''; - - @observable - backendLicense = ''; - @observable recaptchaSiteKey = ''; @@ -148,21 +141,6 @@ export class RootBaseStore { // todo use AppFeature only hasFeature = (feature: string | AppFeature) => 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; - } - - @computed - get isOpenSource(): boolean { - return this.license === GRAFANA_LICENSE_OSS; - } - @action.bound async updateFeatures() { const response = await makeRequest('/features/', {}); diff --git a/grafana-plugin/src/version.ts b/grafana-plugin/src/version.ts new file mode 100644 index 0000000000..b2a27fbc5c --- /dev/null +++ b/grafana-plugin/src/version.ts @@ -0,0 +1,5 @@ +// NOTE: This file is replaced by the release script, so do not expect it to be as it is always. +export const GIT_COMMIT = 'dev'; + +// Declare a constant that will be updated by release-please action +export const CURRENT_VERSION = '1.9.23' as string; // x-release-please-version