From f582b10ee58a2a65a7be1ef81ee6feb189fb2bba Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Wed, 20 Jul 2022 12:47:51 +0200 Subject: [PATCH 01/89] connectivity warning changed the logic and added helper file --- .../DefaultPageLayout/DefaultPageLayout.tsx | 28 +++++++++---------- .../containers/DefaultPageLayout/helper.ts | 7 +++++ 2 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 grafana-plugin/src/containers/DefaultPageLayout/helper.ts diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index 2f4cf07f..af7ed097 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -16,6 +16,7 @@ import sanitize from 'utils/sanitize'; import { getSlackMessage } from './DefaultPageLayout.helpers'; import { SlackError } from './DefaultPageLayout.types'; +import { getIfChatOpsConnected } from './helper'; import styles from './DefaultPageLayout.module.css'; @@ -61,6 +62,9 @@ const DefaultPageLayout: FC = observer((props) => { const { currentTeam } = teamStore; const { currentUser } = userStore; + const isChatOpsConnected = getIfChatOpsConnected(currentUser); + const isPhoneVerified = currentUser?.cloud_connection_status === 3 ? true : currentUser?.verified_phone_number; + return (
@@ -117,9 +121,7 @@ const DefaultPageLayout: FC = observer((props) => { currentTeam && currentUser && store.isUserActionAllowed(UserAction.UpdateOwnSettings) && - (!currentUser.verified_phone_number || - !currentUser.slack_user_identity || - currentUser.cloud_connection_status !== 3) && + (!isPhoneVerified || !isChatOpsConnected) && !getItem(AlertID.CONNECTIVITY_WARNING) ) && ( = observer((props) => { > { <> - {!currentTeam.slack_team_identity && ( + {!isChatOpsConnected && ( <> - Slack Integration is not installed. Please fix it in{' '} - Slack Settings - {'. '} + Communication channels are not connected. Configure at least one channel to receive notifications. + + )} + {!isPhoneVerified && ( + <> + Your phone number is not verified. You can change your configuration in{' '} + User settings )} - {currentUser.cloud_connection_status !== 3 && - !currentUser.verified_phone_number && - 'Your phone number is not verified. '} - {currentTeam.slack_team_identity && - !currentUser.slack_user_identity && - 'Your slack account is not connected. '} - You can change your configuration in{' '} - User settings } diff --git a/grafana-plugin/src/containers/DefaultPageLayout/helper.ts b/grafana-plugin/src/containers/DefaultPageLayout/helper.ts new file mode 100644 index 00000000..5af787af --- /dev/null +++ b/grafana-plugin/src/containers/DefaultPageLayout/helper.ts @@ -0,0 +1,7 @@ +import React from 'react'; + +import { User } from 'models/user/user.types'; + +export const getIfChatOpsConnected = (user: User) => { + return user?.slack_user_identity || user?.telegram_configuration; +}; From 3292ffc9c10bd44433236bf841e01a1509a23086 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Wed, 20 Jul 2022 16:24:01 +0200 Subject: [PATCH 02/89] fix the import --- .../src/containers/DefaultPageLayout/DefaultPageLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index af7ed097..fe067063 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -16,7 +16,7 @@ import sanitize from 'utils/sanitize'; import { getSlackMessage } from './DefaultPageLayout.helpers'; import { SlackError } from './DefaultPageLayout.types'; -import { getIfChatOpsConnected } from './helper'; +import { getIfChatOpsConnected } from 'containers/DefaultPageLayout/helper'; import styles from './DefaultPageLayout.module.css'; From 67465168b8551ebdc886013268abae550d20c564 Mon Sep 17 00:00:00 2001 From: Manu Vamadevan Date: Mon, 8 Aug 2022 20:17:31 +0200 Subject: [PATCH 03/89] 343: node selection for helm chart - engine only - Initial commit --- helm/oncall/templates/engine/deployment.yaml | 12 ++++++++++++ helm/oncall/values.yaml | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/helm/oncall/templates/engine/deployment.yaml b/helm/oncall/templates/engine/deployment.yaml index 9c9d0f76..262b403a 100644 --- a/helm/oncall/templates/engine/deployment.yaml +++ b/helm/oncall/templates/engine/deployment.yaml @@ -71,3 +71,15 @@ spec: timeoutSeconds: 3 resources: {{- toYaml .Values.engine.resources | nindent 12 }} + {{- with .Values.grafana.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.grafana.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.grafana.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/oncall/values.yaml b/helm/oncall/values.yaml index 95ab565e..d49fd25b 100644 --- a/helm/oncall/values.yaml +++ b/helm/oncall/values.yaml @@ -161,6 +161,18 @@ grafana: enabled: true plugins: - grafana-oncall-app + ## @param affinity Affinity for pod assignment + ## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + ## + affinity: { } + ## @param nodeSelector Node labels for pod assignment + ## ref: https://kubernetes.io/docs/user-guide/node-selection/ + ## + nodeSelector: { } + ## @param tolerations Tolerations for pod assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + ## + tolerations: [ ] nameOverride: "" fullnameOverride: "" From 9495ee9f140d570a08d406d0ada2aede576e23b0 Mon Sep 17 00:00:00 2001 From: Manu Vamadevan Date: Mon, 8 Aug 2022 23:25:00 +0200 Subject: [PATCH 04/89] 343: node selection for helm chart - engine only - corrected config positioning --- helm/oncall/templates/engine/deployment.yaml | 6 ++--- helm/oncall/values.yaml | 24 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/helm/oncall/templates/engine/deployment.yaml b/helm/oncall/templates/engine/deployment.yaml index 262b403a..dc836feb 100644 --- a/helm/oncall/templates/engine/deployment.yaml +++ b/helm/oncall/templates/engine/deployment.yaml @@ -71,15 +71,15 @@ spec: timeoutSeconds: 3 resources: {{- toYaml .Values.engine.resources | nindent 12 }} - {{- with .Values.grafana.nodeSelector }} + {{- with .Values.engine.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.grafana.affinity }} + {{- with .Values.engine.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.grafana.tolerations }} + {{- with .Values.engine.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} diff --git a/helm/oncall/values.yaml b/helm/oncall/values.yaml index d49fd25b..9d753f83 100644 --- a/helm/oncall/values.yaml +++ b/helm/oncall/values.yaml @@ -28,6 +28,18 @@ engine: # requests: # cpu: 100m # memory: 128Mi + ## @param affinity Affinity for pod assignment + ## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + ## + affinity: { } + ## @param nodeSelector Node labels for pod assignment + ## ref: https://kubernetes.io/docs/user-guide/node-selection/ + ## + nodeSelector: { } + ## @param tolerations Tolerations for pod assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + ## + tolerations: [ ] # Celery workers pods configuration celery: @@ -161,18 +173,6 @@ grafana: enabled: true plugins: - grafana-oncall-app - ## @param affinity Affinity for pod assignment - ## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity - ## - affinity: { } - ## @param nodeSelector Node labels for pod assignment - ## ref: https://kubernetes.io/docs/user-guide/node-selection/ - ## - nodeSelector: { } - ## @param tolerations Tolerations for pod assignment - ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ - ## - tolerations: [ ] nameOverride: "" fullnameOverride: "" From b9b36b3393933f7623002f018dc321d4bca0dfb2 Mon Sep 17 00:00:00 2001 From: Manu Vamadevan Date: Tue, 9 Aug 2022 10:08:27 +0200 Subject: [PATCH 05/89] 343: node selection for helm chart - engine only - added blank line for seperation --- helm/oncall/values.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/oncall/values.yaml b/helm/oncall/values.yaml index 9d753f83..27af56e5 100644 --- a/helm/oncall/values.yaml +++ b/helm/oncall/values.yaml @@ -28,6 +28,7 @@ engine: # requests: # cpu: 100m # memory: 128Mi + ## @param affinity Affinity for pod assignment ## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity ## From 07388a90adc005ed2edf42c633efeea2f03f1734 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Thu, 8 Sep 2022 11:37:52 +0200 Subject: [PATCH 06/89] Users from search results have been added to the state of options --- .../src/containers/RemoteSelect/RemoteSelect.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx b/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx index 7f061eb5..bc3e47e7 100644 --- a/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx +++ b/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; import { SelectableValue } from '@grafana/data'; import { AsyncMultiSelect, AsyncSelect } from '@grafana/ui'; @@ -48,8 +48,6 @@ const RemoteSelect = inject('store')( openMenuOnFocus = true, } = props; - const [options, setOptions] = useState(); - const getOptions = (data: any[]) => { return data.map((option: any) => ({ value: option[valueField], @@ -58,6 +56,13 @@ const RemoteSelect = inject('store')( })); }; + function mergeOptions(oldOptions: SelectableValue[], newOptions: SelectableValue[]) { + const existedValues = oldOptions.map((o) => o.value); + return newOptions.filter(({ value }) => !existedValues.includes(value)).concat(oldOptions); + } + + const [options, setOptions] = useReducer(mergeOptions, []); + useEffect(() => { makeRequest(href, {}).then((data) => { setOptions(getOptions(data.results || data)); @@ -65,7 +70,10 @@ const RemoteSelect = inject('store')( }, []); const loadOptionsCallback = useCallback((query: string) => { - return makeRequest(href, { params: { search: query } }).then((data) => getOptions(data.results || data)); + return makeRequest(href, { params: { search: query } }).then((data) => { + setOptions(getOptions(data.results || data)); + return getOptions(data.results || data); + }); }, []); const onChangeCallback = useCallback( From f862d0342b041f3a0b053b8577d57c23db035208 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 8 Sep 2022 15:11:38 +0300 Subject: [PATCH 07/89] added helper to reuse wrong team handling functionality --- .../NotFoundInTeam/WrongTeam.helpers.tsx | 23 +++++++++++++++++++ .../src/pages/incident/Incident.tsx | 19 ++------------- .../src/pages/incidents/Incidents.tsx | 7 ++---- 3 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 grafana-plugin/src/components/NotFoundInTeam/WrongTeam.helpers.tsx diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeam.helpers.tsx b/grafana-plugin/src/components/NotFoundInTeam/WrongTeam.helpers.tsx new file mode 100644 index 00000000..176cbf29 --- /dev/null +++ b/grafana-plugin/src/components/NotFoundInTeam/WrongTeam.helpers.tsx @@ -0,0 +1,23 @@ +interface WrongTeamResponse { + notFound?: boolean; + wrongTeamError?: boolean; + teamToSwitch?: { name: string; id: string }; + wrongTeamNoPermissions?: boolean; +} + +export function getWrongTeamResponseInfo({ response }): WrongTeamResponse { + if (response) { + if (response.status === 404) { + return { notFound: true }; + } else if (response.status === 403 && response.data.error_code === 'wrong_team') { + let res = response.data; + if (res.owner_team) { + return { wrongTeamError: true, teamToSwitch: { name: res.owner_team.name, id: res.owner_team.id } }; + } else { + return { wrongTeamError: true, wrongTeamNoPermissions: true }; + } + } + } + + return { notFound: true }; +} diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index ca4eb213..ae8c3574 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -27,6 +27,7 @@ import Collapse from 'components/Collapse/Collapse'; import Block from 'components/GBlock/Block'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; import PluginLink from 'components/PluginLink/PluginLink'; import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; @@ -98,23 +99,7 @@ class IncidentPage extends React.Component query: { id }, } = this.props; - store.alertGroupStore.getAlert(id).catch((error) => { - if (error.response) { - if (error.response.status === 404) { - this.setState({ notFound: true }); - } else if (error.response.status === 403 && error.response.data.error_code === 'wrong_team') { - let res = error.response.data; - if (res.owner_team) { - this.setState({ wrongTeamError: true, teamToSwitch: { name: res.owner_team.name, id: res.owner_team.id } }); - } else { - this.setState({ wrongTeamError: true, wrongTeamNoPermissions: true }); - } - return; - } - } - - this.setState({ notFound: true }); - }); + store.alertGroupStore.getAlert(id).catch((error) => this.setState({ ...getWrongTeamResponseInfo(error) })); }; render() { diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index e2839858..d24f2f30 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -3,14 +3,12 @@ import React, { ReactElement, SyntheticEvent } from 'react'; import { AppRootProps } from '@grafana/data'; import { getLocationSrv } from '@grafana/runtime'; import { Button, Icon, Tooltip, VerticalGroup, LoadingPlaceholder, HorizontalGroup } from '@grafana/ui'; -import { capitalCase } from 'change-case'; import cn from 'classnames/bind'; import { get } from 'lodash-es'; import { observer } from 'mobx-react'; import moment from 'moment'; import Emoji from 'react-emoji-render'; -import CardButton from 'components/CardButton/CardButton'; import CursorPagination from 'components/CursorPagination/CursorPagination'; import GTable from 'components/GTable/GTable'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; @@ -21,12 +19,11 @@ import { TutorialStep } from 'components/Tutorial/Tutorial.types'; import { IncidentsFiltersType } from 'containers/IncidentsFilters/IncidentFilters.types'; import IncidentsFilters from 'containers/IncidentsFilters/IncidentsFilters'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; -import { MaintenanceIntegration } from 'models/alert_receive_channel'; -import { Alert, Alert as AlertType, AlertAction, IncidentStatus } from 'models/alertgroup/alertgroup.types'; +import { Alert, Alert as AlertType, AlertAction } from 'models/alertgroup/alertgroup.types'; import { User } from 'models/user/user.types'; import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from 'pages/incident/Incident.helpers'; import { move } from 'state/helpers'; -import { SelectOption, WithStoreProps } from 'state/types'; +import { WithStoreProps } from 'state/types'; import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; From 3e5d9e3fcd77cc4217ed29bf8dc71d1c0512bb61 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 9 Sep 2022 13:59:59 +0300 Subject: [PATCH 08/89] handling for escalation chains for wrong team --- grafana-plugin/src/models/base_store.ts | 8 ++- .../escalation_chain/escalation_chain.ts | 16 ++++- grafana-plugin/src/models/user/user.ts | 4 +- grafana-plugin/src/network/index.ts | 20 +++++++ .../escalation-chains/EscalationChains.tsx | 60 ++++++++++++------- .../src/pages/integrations/Integrations.tsx | 10 ++++ grafana-plugin/src/pages/users/Users.tsx | 44 ++++++++++---- 7 files changed, 122 insertions(+), 40 deletions(-) diff --git a/grafana-plugin/src/models/base_store.ts b/grafana-plugin/src/models/base_store.ts index ab46fe3c..90e6a76e 100644 --- a/grafana-plugin/src/models/base_store.ts +++ b/grafana-plugin/src/models/base_store.ts @@ -13,7 +13,9 @@ export default class BaseStore { this.rootStore = rootStore; } - onApiError(error: any) { + onApiError(error: any, skipErrorHandling: boolean = false) { + if (skipErrorHandling) throw error + if (error.response.status >= 400 && error.response.status < 500) { const payload = error.response.data; const text = @@ -37,10 +39,10 @@ export default class BaseStore { } @action - async getById(id: string) { + async getById(id: string, skipErrorHandling: boolean = false) { return await makeRequest(`${this.path}${id}/`, { method: 'GET', - }).catch(this.onApiError); + }).catch((error) => this.onApiError(error, skipErrorHandling)); } @action diff --git a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts index b0e7a4e7..820473a5 100644 --- a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts +++ b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts @@ -22,6 +22,18 @@ export class EscalationChainStore extends BaseStore { this.path = '/escalation_chains/'; } + @action + async loadItem(id: EscalationChain['id'], skipErrorHandling: boolean = false) { + const escalationChain = await this.getById(id, skipErrorHandling); + + this.items = { + ...this.items, + [id]: escalationChain + } + + return escalationChain + } + @action async updateById(id: EscalationChain['id']) { const response = await this.getById(id); @@ -53,9 +65,9 @@ export class EscalationChainStore extends BaseStore { } @action - async updateItems(query = '') { + async updateItems(query = '', id?: string) { const results = await makeRequest(`${this.path}`, { - params: { search: query }, + params: { search: query, id }, }); this.items = { diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index 2a388913..c1961844 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -60,8 +60,8 @@ export class UserStore extends BaseStore { } @action - async loadUser(userPk: User['pk']) { - const user = await this.getById(userPk); + async loadUser(userPk: User['pk'], skipErrorHandling: boolean = false) { + const user = await this.getById(userPk, skipErrorHandling); this.items = { ...this.items, diff --git a/grafana-plugin/src/network/index.ts b/grafana-plugin/src/network/index.ts index aa35fb3b..96882fd1 100644 --- a/grafana-plugin/src/network/index.ts +++ b/grafana-plugin/src/network/index.ts @@ -30,11 +30,31 @@ interface RequestConfig { validateStatus?: (status: number) => boolean; } +const failPaths = [ + 'api/plugin-proxy/grafana-oncall-app/api/internal/v1/users/URPAN2A31CVWQ/', + 'api/plugin-proxy/grafana-oncall-app/api/internal/v1/escalation_chains/FDF7ZQMNKYIQK/', +]; + export const makeRequest = async (path: string, config: RequestConfig) => { const { method = 'GET', params, data, validateStatus } = config; const url = `${API_PROXY_PREFIX}${API_PATH_PREFIX}${path}`; + if (failPaths.includes(url)) { + throw { + response: { + status: 403, + data: { + error_code: 'wrong_team', + owner_team: { + name: 'Rares', + id: '14999718', + }, + }, + }, + }; + } + const response = await instance({ method, url, diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 42920ba7..375d810c 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -2,17 +2,7 @@ import React from 'react'; import { AppRootProps } from '@grafana/data'; import { getLocationSrv } from '@grafana/runtime'; -import { - Alert, - Button, - EmptySearchResult, - HorizontalGroup, - Icon, - IconButton, - LoadingPlaceholder, - Tooltip, - VerticalGroup, -} from '@grafana/ui'; +import { Button, HorizontalGroup, Icon, IconButton, LoadingPlaceholder, Tooltip, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; import { debounce } from 'lodash-es'; import { observer } from 'mobx-react'; @@ -21,28 +11,24 @@ import Collapse from 'components/Collapse/Collapse'; import EscalationsFilters from 'components/EscalationsFilters/EscalationsFilters'; import Block from 'components/GBlock/Block'; import GList from 'components/GList/GList'; -import IntegrationsFilters from 'components/IntegrationsFilters/IntegrationsFilters'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; import WithConfirm from 'components/WithConfirm/WithConfirm'; -import AlertReceiveChannelCard from 'containers/AlertReceiveChannelCard/AlertReceiveChannelCard'; import EscalationChainCard from 'containers/EscalationChainCard/EscalationChainCard'; import EscalationChainForm from 'containers/EscalationChainForm/EscalationChainForm'; import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps'; -import GSelect from 'containers/GSelect/GSelect'; -import { IntegrationSettingsTab } from 'containers/IntegrationSettings/IntegrationSettings.types'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; -import { AlertReceiveChannel } from 'models/alert_receive_channel'; -import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'; -import { SelectOption, WithStoreProps } from 'state/types'; +import { WithStoreProps } from 'state/types'; import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import { openWarningNotification } from 'utils'; import styles from './EscalationChains.module.css'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; const cx = cn.bind(styles); @@ -53,6 +39,11 @@ interface EscalationChainsPageState { showCreateEscalationChainModal: boolean; escalationChainIdToCopy: EscalationChain['id']; selectedEscalationChain: EscalationChain['id']; + + notFound?: boolean; + wrongTeamError?: boolean; + teamToSwitch?: { name: string; id: string }; + wrongTeamNoPermissions?: boolean; } export interface Filters { @@ -66,13 +57,15 @@ class EscalationChainsPage extends React.Component { + parseQueryParams = async () => { const { store, query } = this.props; const { escalationChainsFilters: { searchTerm }, @@ -82,9 +75,14 @@ class EscalationChainsPage extends React.Component this.setState({ ...getWrongTeamResponseInfo(error) })); + if (!escalationChain) return; + + escalationChain = escalationChainStore.items[query.id]; if (escalationChain) { selectedEscalationChain = escalationChain.id; } else { @@ -115,7 +113,7 @@ class EscalationChainsPage extends React.Component { const { store } = this.props; - return store.escalationChainStore.updateItems(); + return store.escalationChainStore.updateItems('', this.props.query.id); }; componentDidUpdate() {} @@ -127,8 +125,26 @@ class EscalationChainsPage extends React.Component + ); + } + const { escalationChainStore } = store; const searchResult = escalationChainStore.getSearchResult(escalationChainsFilters.searchTerm); diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index fa331dc5..235b0846 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -26,6 +26,7 @@ import { withMobXProviderContext } from 'state/withStore'; import { openWarningNotification } from 'utils'; import styles from './Integrations.module.css'; +import { convertRelativeToAbsoluteDate } from 'utils/datetime'; const cx = cn.bind(styles); @@ -34,6 +35,11 @@ interface IntegrationsState { showCreateIntegrationModal: boolean; alertReceiveChannelToShowSettings?: AlertReceiveChannel['id']; integrationSettingsTab?: IntegrationSettingsTab; + + notFound?: boolean; + wrongTeamError?: boolean; + teamToSwitch?: { name: string; id: string }; + wrongTeamNoPermissions?: boolean; } interface IntegrationsProps extends WithStoreProps, AppRootProps {} @@ -43,6 +49,9 @@ class Integrations extends React.Component state: IntegrationsState = { integrationsFilters: { searchTerm: '' }, showCreateIntegrationModal: false, + + wrongTeamError: false, + wrongTeamNoPermissions: false, }; alertReceiveChanneltoPoll: { [key: string]: number } = {}; @@ -65,6 +74,7 @@ class Integrations extends React.Component const searchResult = alertReceiveChannelStore.getSearchResult(); let selectedAlertReceiveChannel = store.selectedAlertReceiveChannel; + if (query.id) { const alertReceiveChannelId = searchResult && searchResult.find((res) => res.id === query?.id)?.id; if (alertReceiveChannelId) { diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index a8ab3c5b..01a8a6d9 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -14,10 +14,8 @@ import Text from 'components/Text/Text'; import UsersFilters from 'components/UsersFilters/UsersFilters'; import UserSettings from 'containers/UserSettings/UserSettings'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; -import { CrossCircleIcon } from 'icons'; import { getRole } from 'models/user/user.helpers'; -import { User, User as UserType, UserRole } from 'models/user/user.types'; -import { AppFeature } from 'state/features'; +import { User as UserType, UserRole } from 'models/user/user.types'; import { WithStoreProps } from 'state/types'; import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; @@ -25,6 +23,8 @@ import { withMobXProviderContext } from 'state/withStore'; import { getRealFilters, getUserRowClassNameFn } from './Users.helpers'; import styles from './Users.module.css'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; const cx = cn.bind(styles); @@ -34,29 +34,38 @@ const ITEMS_PER_PAGE = 100; interface UsersState { page: number; + isWrongTeam: boolean; userPkToEdit?: UserType['pk'] | 'new'; usersFilters?: { searchTerm: string; roles?: UserRole[]; }; + + notFound?: boolean; + wrongTeamError?: boolean; + teamToSwitch?: { name: string; id: string }; + wrongTeamNoPermissions?: boolean; } @observer class Users extends React.Component { state: UsersState = { page: 1, + isWrongTeam: false, userPkToEdit: undefined, usersFilters: { searchTerm: '', roles: [UserRole.ADMIN, UserRole.EDITOR, UserRole.VIEWER], }, + + wrongTeamError: false, + wrongTeamNoPermissions: false, }; initialUsersLoaded = false; async componentDidMount() { const { - store, query: { p }, } = this.props; this.setState({ page: p ? Number(p) : 1 }, this.updateUsers); @@ -97,7 +106,9 @@ class Users extends React.Component { } = this.props; if (id) { - await (id === 'me' ? store.userStore.loadCurrentUser() : store.userStore.loadUser(String(id))); + await (id === 'me' ? store.userStore.loadCurrentUser() : store.userStore.loadUser(String(id), true)).catch( + (error) => this.setState({ ...getWrongTeamResponseInfo(error) }) + ); const userPkToEdit = String(id === 'me' ? store.userStore.currentUserPk : id); @@ -108,7 +119,7 @@ class Users extends React.Component { }; render() { - const { usersFilters, userPkToEdit, page } = this.state; + const { usersFilters, userPkToEdit, page, wrongTeamError, teamToSwitch, wrongTeamNoPermissions } = this.state; const { store } = this.props; const { userStore } = store; @@ -131,11 +142,6 @@ class Users extends React.Component { key: 'note', render: this.renderNote, }, - // { - // width: '15%', - // key: 'contacts', - // render: this.renderContacts, - // }, { width: '20%', title: 'Default Notifications', @@ -154,6 +160,7 @@ class Users extends React.Component { render: this.renderButtons, }, ]; + const handleClear = () => this.setState( { usersFilters: { searchTerm: '', roles: [UserRole.ADMIN, UserRole.EDITOR, UserRole.VIEWER] } }, @@ -164,6 +171,21 @@ class Users extends React.Component { const { count, results } = userStore.getSearchResult(); + if (wrongTeamError) { + const currentTeamId = store.userStore.currentUser?.current_team; + const currentTeamName = store.grafanaTeamStore.items[currentTeamId]?.name; + + return ( + + ); + } + return (
From e72be018fc5df63030d79ee51cffd03ebfc27e09 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Fri, 9 Sep 2022 13:11:44 +0200 Subject: [PATCH 09/89] lint fixes --- .../src/components/AlertTemplates/AlertTemplatesForm.tsx | 4 ++-- grafana-plugin/src/components/Collapse/Collapse.tsx | 2 +- grafana-plugin/src/components/PluginLink/PluginLink.tsx | 2 +- grafana-plugin/src/components/SourceCode/SourceCode.tsx | 2 +- grafana-plugin/src/components/Text/Text.tsx | 2 +- .../src/components/VerticalTabsBar/VerticalTabsBar.tsx | 2 +- .../containers/DefaultPageLayout/DefaultPageLayout.tsx | 8 ++++++-- .../src/containers/PluginConfigPage/PluginConfigPage.tsx | 6 +++++- 8 files changed, 18 insertions(+), 10 deletions(-) diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx index e709bbc6..eaecbbfd 100644 --- a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx +++ b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx @@ -244,8 +244,8 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
Please note that after changing the web title template new alert groups will be searchable by - new title. Alert groups created before the template was changed will be still searchable by - old title only. + new title. Alert groups created before the template was changed will be still searchable by old + title only.
)} diff --git a/grafana-plugin/src/components/Collapse/Collapse.tsx b/grafana-plugin/src/components/Collapse/Collapse.tsx index 959898d6..940507aa 100644 --- a/grafana-plugin/src/components/Collapse/Collapse.tsx +++ b/grafana-plugin/src/components/Collapse/Collapse.tsx @@ -15,7 +15,7 @@ interface CollapseProps { className?: string; contentClassName?: string; headerWithBackground?: boolean; - children?: any + children?: any; } const cx = cn.bind(styles); diff --git a/grafana-plugin/src/components/PluginLink/PluginLink.tsx b/grafana-plugin/src/components/PluginLink/PluginLink.tsx index eef5374e..3a218c2e 100644 --- a/grafana-plugin/src/components/PluginLink/PluginLink.tsx +++ b/grafana-plugin/src/components/PluginLink/PluginLink.tsx @@ -11,7 +11,7 @@ interface PluginLinkProps extends LocationUpdate { disabled?: boolean; className?: string; wrap?: boolean; - children: any + children: any; } const cx = cn.bind(styles); diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.tsx b/grafana-plugin/src/components/SourceCode/SourceCode.tsx index 91c3513c..0196ebf3 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.tsx +++ b/grafana-plugin/src/components/SourceCode/SourceCode.tsx @@ -13,7 +13,7 @@ const cx = cn.bind(styles); interface SourceCodeProps { noMaxHeight?: boolean; showCopyToClipboard?: boolean; - children?: any + children?: any; } const SourceCode: FC = (props) => { diff --git a/grafana-plugin/src/components/Text/Text.tsx b/grafana-plugin/src/components/Text/Text.tsx index 7d76b937..6cf9b502 100644 --- a/grafana-plugin/src/components/Text/Text.tsx +++ b/grafana-plugin/src/components/Text/Text.tsx @@ -81,7 +81,7 @@ const Text: TextType = (props) => { 'text--strong': strong, 'text--underline': underline, 'no-wrap': !wrap, - keyboard + keyboard, })} > {hidden ? PLACEHOLDER : children} diff --git a/grafana-plugin/src/components/VerticalTabsBar/VerticalTabsBar.tsx b/grafana-plugin/src/components/VerticalTabsBar/VerticalTabsBar.tsx index 372f3ad3..ae6ec9c3 100644 --- a/grafana-plugin/src/components/VerticalTabsBar/VerticalTabsBar.tsx +++ b/grafana-plugin/src/components/VerticalTabsBar/VerticalTabsBar.tsx @@ -41,7 +41,7 @@ export default VerticalTabsBar; interface TabProps { id: string; - children?: any + children?: any; } export const VerticalTab: FC = ({ children }) => { diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index 563d1382..002beb7c 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -8,6 +8,7 @@ import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import PluginLink from 'components/PluginLink/PluginLink'; +import { getIfChatOpsConnected } from 'containers/DefaultPageLayout/helper'; import { useStore } from 'state/useStore'; import { UserAction } from 'state/userAction'; import { GRAFANA_LICENSE_OSS } from 'utils/consts'; @@ -17,7 +18,6 @@ import sanitize from 'utils/sanitize'; import { getSlackMessage } from './DefaultPageLayout.helpers'; import { SlackError } from './DefaultPageLayout.types'; -import { getIfChatOpsConnected } from 'containers/DefaultPageLayout/helper'; import styles from './DefaultPageLayout.module.css'; @@ -113,7 +113,11 @@ const DefaultPageLayout: FC = observer((props) => { {`Current plugin version: ${plugin.version}, current engine version: ${store.backendVersion}`}
Please see{' '} - + the update instructions . diff --git a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx index 8d612aa7..98b59cc2 100644 --- a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx +++ b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx @@ -285,7 +285,11 @@ Seek for such a line: “Your invite token: <> , use it in the Graf > <> - + How to re-issue the invite token? From f3edd92a77a2e44408c292715b31e8fdb059574c Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 9 Sep 2022 14:26:17 +0300 Subject: [PATCH 10/89] same for integrations --- .../alert_receive_channel.ts | 12 +++++++ .../escalation_chain/escalation_chain.ts | 2 +- grafana-plugin/src/network/index.ts | 1 + .../src/pages/integrations/Integrations.tsx | 35 ++++++++++++++++--- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index 4e3ef3da..57b6f332 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -70,6 +70,18 @@ export class AlertReceiveChannelStore extends BaseStore { ); } + @action + async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling: boolean = false): Promise { + const alertReceiveChannel = await this.getById(id, skipErrorHandling); + + this.items = { + ...this.items, + [id]: alertReceiveChannel + } + + return alertReceiveChannel + } + @action async updateItems(query = '') { const result = await this.getAll(query); diff --git a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts index 820473a5..dde0b3b4 100644 --- a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts +++ b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts @@ -23,7 +23,7 @@ export class EscalationChainStore extends BaseStore { } @action - async loadItem(id: EscalationChain['id'], skipErrorHandling: boolean = false) { + async loadItem(id: EscalationChain['id'], skipErrorHandling: boolean = false): Promise { const escalationChain = await this.getById(id, skipErrorHandling); this.items = { diff --git a/grafana-plugin/src/network/index.ts b/grafana-plugin/src/network/index.ts index 96882fd1..23fbd379 100644 --- a/grafana-plugin/src/network/index.ts +++ b/grafana-plugin/src/network/index.ts @@ -33,6 +33,7 @@ interface RequestConfig { const failPaths = [ 'api/plugin-proxy/grafana-oncall-app/api/internal/v1/users/URPAN2A31CVWQ/', 'api/plugin-proxy/grafana-oncall-app/api/internal/v1/escalation_chains/FDF7ZQMNKYIQK/', + 'api/plugin-proxy/grafana-oncall-app/api/internal/v1/alert_receive_channels/CSPE3C7R4Q38G/', ]; export const makeRequest = async (path: string, config: RequestConfig) => { diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 235b0846..8cfaea82 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -7,6 +7,7 @@ import cn from 'classnames/bind'; import { debounce } from 'lodash-es'; import { observer } from 'mobx-react'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import GList from 'components/GList/GList'; import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/IntegrationsFilters'; import Text from 'components/Text/Text'; @@ -24,9 +25,9 @@ import { WithStoreProps } from 'state/types'; import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import { openWarningNotification } from 'utils'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; import styles from './Integrations.module.css'; -import { convertRelativeToAbsoluteDate } from 'utils/datetime'; const cx = cn.bind(styles); @@ -67,7 +68,7 @@ class Integrations extends React.Component getLocationSrv().update({ partial: true, query: { id: alertReceiveChannelId } }); }; - parseQueryParams = () => { + parseQueryParams = async () => { const { store, query } = this.props; const { alertReceiveChannelStore } = store; @@ -76,14 +77,19 @@ class Integrations extends React.Component let selectedAlertReceiveChannel = store.selectedAlertReceiveChannel; if (query.id) { - const alertReceiveChannelId = searchResult && searchResult.find((res) => res.id === query?.id)?.id; - if (alertReceiveChannelId) { - selectedAlertReceiveChannel = alertReceiveChannelId; + let alertReceiveChannel = await alertReceiveChannelStore + .loadItem(query.id, true) + .catch((error) => this.setState({ ...getWrongTeamResponseInfo(error) })); + if (!alertReceiveChannel) return; + + if (alertReceiveChannel.id) { + selectedAlertReceiveChannel = alertReceiveChannel.id; } else { openWarningNotification( `Integration with id=${query?.id} is not found. Please select integration from the list.` ); } + if (query.tab) { this.setState({ integrationSettingsTab: query.tab }); this.setState({ alertReceiveChannelToShowSettings: query.id }); @@ -123,7 +129,26 @@ class Integrations extends React.Component alertReceiveChannelToShowSettings, integrationSettingsTab, showCreateIntegrationModal, + wrongTeamError, + teamToSwitch, + wrongTeamNoPermissions, } = this.state; + + if (wrongTeamError) { + const currentTeamId = store.userStore.currentUser?.current_team; + const currentTeamName = store.grafanaTeamStore.items[currentTeamId]?.name; + + return ( + + ); + } + const { alertReceiveChannelStore } = store; const searchResult = alertReceiveChannelStore.getSearchResult(); From aded7a25cb0a38a93cff0a0a790200ce91d4676d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 9 Sep 2022 14:51:13 +0300 Subject: [PATCH 11/89] same for schedules --- .../src/models/schedule/schedule.ts | 12 +++++++ .../src/pages/integrations/Integrations.tsx | 3 -- .../src/pages/schedules/Schedules.tsx | 36 ++++++++++++++++--- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index 14d02744..c424882a 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -25,6 +25,18 @@ export class ScheduleStore extends BaseStore { this.path = '/schedules/'; } + @action + async loadItem(id: Schedule['id'], skipErrorHandling: boolean = false): Promise { + const schedule = await this.getById(id, skipErrorHandling); + + this.items = { + ...this.items, + [id]: schedule, + }; + + return schedule; + } + @action async updateScheduleEvents( scheduleId: Schedule['id'], diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 8cfaea82..7d46c7c1 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -121,9 +121,6 @@ class Integrations extends React.Component render() { const { store } = this.props; - const { - integrationsFilters: { searchTerm }, - } = this.state; const { integrationsFilters, alertReceiveChannelToShowSettings, diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index d538a81d..57ff9511 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -43,6 +43,8 @@ import { openErrorNotification } from 'utils'; import { getDatesString } from './Schedules.helpers'; import styles from './Schedules.module.css'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; const cx = cn.bind(styles); @@ -53,6 +55,11 @@ interface SchedulesPageState { scheduleIdToExport?: Schedule['id']; filters: SchedulesFiltersType; expandedSchedulesKeys: Array; + + notFound?: boolean; + wrongTeamError?: boolean; + teamToSwitch?: { name: string; id: string }; + wrongTeamNoPermissions?: boolean; } @observer @@ -62,6 +69,8 @@ class SchedulesPage extends React.Component this.setState({ ...getWrongTeamResponseInfo(error) })); + if (!schedule) return; + const schedules = store.scheduleStore.getSearchResult(); const scheduleId = schedules && schedules.find((res) => res.id === id)?.id; if (scheduleId || id === 'new') { @@ -101,8 +115,23 @@ class SchedulesPage extends React.Component + ); + } const columns = [ { @@ -148,9 +177,6 @@ class SchedulesPage extends React.Component
From 1a778f95c24de310850508d8facd792156ec69a6 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 9 Sep 2022 15:17:28 +0300 Subject: [PATCH 12/89] webhooks also --- .../outgoing_webhook/outgoing_webhook.ts | 12 +++++++ .../outgoing_webhooks/OutgoingWebhooks.tsx | 35 ++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts index ab70799a..a5e70b59 100644 --- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts +++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts @@ -19,6 +19,18 @@ export class OutgoingWebhookStore extends BaseStore { this.path = '/custom_buttons/'; } + @action + async loadItem(id: OutgoingWebhook['id'], skipErrorHandling: boolean = false): Promise { + const outgoingWebhook = await this.getById(id, skipErrorHandling); + + this.items = { + ...this.items, + [id]: outgoingWebhook, + }; + + return outgoingWebhook; + } + @action async updateById(id: OutgoingWebhook['id']) { const response = await this.getById(id); diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 6ced40da..0c427e75 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -21,6 +21,8 @@ import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import styles from './OutgoingWebhooks.module.css'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; const cx = cn.bind(styles); @@ -28,11 +30,18 @@ interface OutgoingWebhooksProps extends WithStoreProps, AppRootProps {} interface OutgoingWebhooksState { outgoingWebhookIdToEdit?: OutgoingWebhook['id'] | 'new'; + notFound?: boolean; + wrongTeamError?: boolean; + teamToSwitch?: { name: string; id: string }; + wrongTeamNoPermissions?: boolean; } @observer class OutgoingWebhooks extends React.Component { - state: OutgoingWebhooksState = {}; + state: OutgoingWebhooksState = { + wrongTeamError: false, + wrongTeamNoPermissions: false, + }; async componentDidMount() { this.update().then(this.parseQueryParams); @@ -44,12 +53,16 @@ class OutgoingWebhooks extends React.Component { + parseQueryParams = async () => { const { store, query: { id }, } = this.props; + await store.outgoingWebhookStore + .loadItem(id, true) + .catch((error) => this.setState({ ...getWrongTeamResponseInfo(error) })); + if (id) { this.setState({ outgoingWebhookIdToEdit: id }); } @@ -57,14 +70,28 @@ class OutgoingWebhooks extends React.Component { const { store } = this.props; - const { selectedAlertReceiveChannel } = store; return store.outgoingWebhookStore.updateItems(); }; render() { const { store } = this.props; - const { outgoingWebhookIdToEdit } = this.state; + const { outgoingWebhookIdToEdit, wrongTeamError, teamToSwitch, wrongTeamNoPermissions } = this.state; + + if (wrongTeamError) { + const currentTeamId = store.userStore.currentUser?.current_team; + const currentTeamName = store.grafanaTeamStore.items[currentTeamId]?.name; + + return ( + + ); + } const webhooks = store.outgoingWebhookStore.getSearchResult(); From ced13786df84671dc4b587a2c6a55b13f6776c4e Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 9 Sep 2022 16:03:11 +0300 Subject: [PATCH 13/89] refactored usage of wrong team display --- .../src/components/NotFoundInTeam/WrongTeamStub.tsx | 7 +++++-- .../alert_receive_channel/alert_receive_channel.ts | 2 +- grafana-plugin/src/models/base_store.ts | 6 +++--- .../src/models/escalation_chain/escalation_chain.ts | 2 +- .../src/models/outgoing_webhook/outgoing_webhook.ts | 2 +- grafana-plugin/src/models/schedule/schedule.ts | 2 +- grafana-plugin/src/models/user/user.ts | 2 +- .../src/pages/escalation-chains/EscalationChains.tsx | 8 ++------ grafana-plugin/src/pages/incident/Incident.tsx | 6 ++---- grafana-plugin/src/pages/integrations/Integrations.tsx | 10 +++------- .../src/pages/outgoing_webhooks/OutgoingWebhooks.tsx | 8 ++------ grafana-plugin/src/pages/schedules/Schedules.tsx | 10 +++------- grafana-plugin/src/pages/users/Users.tsx | 8 ++------ 13 files changed, 27 insertions(+), 46 deletions(-) diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx index c28d8eb8..269c07a8 100644 --- a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx +++ b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx @@ -17,14 +17,17 @@ export interface WrongTeamStubProps { className?: string; objectName: string; pageName: string; - currentTeam?: string; switchToTeam?: { name: string; id: string }; wrongTeamNoPermissions?: boolean; } const WrongTeamStub: FC = (props) => { const store = useStore(); - const { objectName, pageName, currentTeam, switchToTeam, className, wrongTeamNoPermissions } = props; + + const currentTeamId = store.userStore.currentUser?.current_team; + const currentTeam = store.grafanaTeamStore.items[currentTeamId]?.name; + + const { objectName, pageName, switchToTeam, wrongTeamNoPermissions } = props; const onTeamChange = async (teamId: GrafanaTeam['id']) => { await store.userStore.updateCurrentUser({ current_team: teamId }); diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index 57b6f332..ab8808cf 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -71,7 +71,7 @@ export class AlertReceiveChannelStore extends BaseStore { } @action - async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling: boolean = false): Promise { + async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling = false): Promise { const alertReceiveChannel = await this.getById(id, skipErrorHandling); this.items = { diff --git a/grafana-plugin/src/models/base_store.ts b/grafana-plugin/src/models/base_store.ts index 90e6a76e..56cebb15 100644 --- a/grafana-plugin/src/models/base_store.ts +++ b/grafana-plugin/src/models/base_store.ts @@ -13,8 +13,8 @@ export default class BaseStore { this.rootStore = rootStore; } - onApiError(error: any, skipErrorHandling: boolean = false) { - if (skipErrorHandling) throw error + onApiError(error: any, skipErrorHandling = false) { + if (skipErrorHandling) {throw error} if (error.response.status >= 400 && error.response.status < 500) { const payload = error.response.data; @@ -39,7 +39,7 @@ export default class BaseStore { } @action - async getById(id: string, skipErrorHandling: boolean = false) { + async getById(id: string, skipErrorHandling = false) { return await makeRequest(`${this.path}${id}/`, { method: 'GET', }).catch((error) => this.onApiError(error, skipErrorHandling)); diff --git a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts index dde0b3b4..a64ed47b 100644 --- a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts +++ b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts @@ -23,7 +23,7 @@ export class EscalationChainStore extends BaseStore { } @action - async loadItem(id: EscalationChain['id'], skipErrorHandling: boolean = false): Promise { + async loadItem(id: EscalationChain['id'], skipErrorHandling = false): Promise { const escalationChain = await this.getById(id, skipErrorHandling); this.items = { diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts index a5e70b59..d064fa38 100644 --- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts +++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts @@ -20,7 +20,7 @@ export class OutgoingWebhookStore extends BaseStore { } @action - async loadItem(id: OutgoingWebhook['id'], skipErrorHandling: boolean = false): Promise { + async loadItem(id: OutgoingWebhook['id'], skipErrorHandling = false): Promise { const outgoingWebhook = await this.getById(id, skipErrorHandling); this.items = { diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index c424882a..a2b66973 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -26,7 +26,7 @@ export class ScheduleStore extends BaseStore { } @action - async loadItem(id: Schedule['id'], skipErrorHandling: boolean = false): Promise { + async loadItem(id: Schedule['id'], skipErrorHandling = false): Promise { const schedule = await this.getById(id, skipErrorHandling); this.items = { diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index c1961844..22dbd1f7 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -60,7 +60,7 @@ export class UserStore extends BaseStore { } @action - async loadUser(userPk: User['pk'], skipErrorHandling: boolean = false) { + async loadUser(userPk: User['pk'], skipErrorHandling = false) { const user = await this.getById(userPk, skipErrorHandling); this.items = { diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 375d810c..d9cd7458 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -12,6 +12,7 @@ import EscalationsFilters from 'components/EscalationsFilters/EscalationsFilters import Block from 'components/GBlock/Block'; import GList from 'components/GList/GList'; import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; @@ -28,7 +29,6 @@ import { withMobXProviderContext } from 'state/withStore'; import { openWarningNotification } from 'utils'; import styles from './EscalationChains.module.css'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; const cx = cn.bind(styles); @@ -80,7 +80,7 @@ class EscalationChainsPage extends React.Component this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!escalationChain) return; + if (!escalationChain) {return;} escalationChain = escalationChainStore.items[query.id]; if (escalationChain) { @@ -131,14 +131,10 @@ class EscalationChainsPage extends React.Component diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index ae8c3574..407c6004 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -26,8 +26,8 @@ import reactStringReplace from 'react-string-replace'; import Collapse from 'components/Collapse/Collapse'; import Block from 'components/GBlock/Block'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import PluginLink from 'components/PluginLink/PluginLink'; import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; @@ -122,8 +122,7 @@ class IncidentPage extends React.Component const { alerts } = store.alertGroupStore; const incident = alerts.get(id); - const currentTeamId = store.userStore.currentUser?.current_team; - const currentTeamName = store.grafanaTeamStore.items[currentTeamId]?.name; + if (notFound) { return (
@@ -147,7 +146,6 @@ class IncidentPage extends React.Component diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 7d46c7c1..08d069d9 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -7,9 +7,10 @@ import cn from 'classnames/bind'; import { debounce } from 'lodash-es'; import { observer } from 'mobx-react'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import GList from 'components/GList/GList'; import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/IntegrationsFilters'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; @@ -25,7 +26,6 @@ import { WithStoreProps } from 'state/types'; import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import { openWarningNotification } from 'utils'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; import styles from './Integrations.module.css'; @@ -80,7 +80,7 @@ class Integrations extends React.Component let alertReceiveChannel = await alertReceiveChannelStore .loadItem(query.id, true) .catch((error) => this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!alertReceiveChannel) return; + if (!alertReceiveChannel) {return;} if (alertReceiveChannel.id) { selectedAlertReceiveChannel = alertReceiveChannel.id; @@ -132,14 +132,10 @@ class Integrations extends React.Component } = this.state; if (wrongTeamError) { - const currentTeamId = store.userStore.currentUser?.current_team; - const currentTeamName = store.grafanaTeamStore.items[currentTeamId]?.name; - return ( diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 0c427e75..061f5074 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -7,6 +7,8 @@ import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import GTable from 'components/GTable/GTable'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import WithConfirm from 'components/WithConfirm/WithConfirm'; @@ -21,8 +23,6 @@ import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import styles from './OutgoingWebhooks.module.css'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; const cx = cn.bind(styles); @@ -79,14 +79,10 @@ class OutgoingWebhooks extends React.Component diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 57ff9511..62e3dfef 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -22,6 +22,8 @@ import moment, { Moment } from 'moment-timezone'; import instructionsImage from 'assets/img/events_instructions.png'; import Avatar from 'components/Avatar/Avatar'; import GTable from 'components/GTable/GTable'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import PluginLink from 'components/PluginLink/PluginLink'; import SchedulesFilters from 'components/SchedulesFilters/SchedulesFilters'; import { SchedulesFiltersType } from 'components/SchedulesFilters/SchedulesFilters.types'; @@ -43,8 +45,6 @@ import { openErrorNotification } from 'utils'; import { getDatesString } from './Schedules.helpers'; import styles from './Schedules.module.css'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; const cx = cn.bind(styles); @@ -93,7 +93,7 @@ class SchedulesPage extends React.Component this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!schedule) return; + if (!schedule) {return;} const schedules = store.scheduleStore.getSearchResult(); const scheduleId = schedules && schedules.find((res) => res.id === id)?.id; @@ -119,14 +119,10 @@ class SchedulesPage extends React.Component diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index 01a8a6d9..6c9b961f 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -9,6 +9,8 @@ import { observer } from 'mobx-react'; import Avatar from 'components/Avatar/Avatar'; import GTable from 'components/GTable/GTable'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; +import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import UsersFilters from 'components/UsersFilters/UsersFilters'; @@ -23,8 +25,6 @@ import { withMobXProviderContext } from 'state/withStore'; import { getRealFilters, getUserRowClassNameFn } from './Users.helpers'; import styles from './Users.module.css'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; const cx = cn.bind(styles); @@ -172,14 +172,10 @@ class Users extends React.Component { const { count, results } = userStore.getSearchResult(); if (wrongTeamError) { - const currentTeamId = store.userStore.currentUser?.current_team; - const currentTeamName = store.grafanaTeamStore.items[currentTeamId]?.name; - return ( From 812ab7b185e6139777749bf8d1c3408357fbb962 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 12 Sep 2022 10:36:04 +0300 Subject: [PATCH 14/89] added a comment to let it know about re-throwing error --- grafana-plugin/src/models/base_store.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/grafana-plugin/src/models/base_store.ts b/grafana-plugin/src/models/base_store.ts index 56cebb15..03115181 100644 --- a/grafana-plugin/src/models/base_store.ts +++ b/grafana-plugin/src/models/base_store.ts @@ -14,7 +14,9 @@ export default class BaseStore { } onApiError(error: any, skipErrorHandling = false) { - if (skipErrorHandling) {throw error} + if (skipErrorHandling) { + throw error; // rethrow error and skip additional handling like showing notification + } if (error.response.status >= 400 && error.response.status < 500) { const payload = error.response.data; From 6f191518df4de1c7528667d57fd3cf1a1927398d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 12 Sep 2022 10:44:22 +0300 Subject: [PATCH 15/89] removed unused param --- .../src/models/escalation_chain/escalation_chain.ts | 4 ++-- .../src/pages/escalation-chains/EscalationChains.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts index a64ed47b..c6e19968 100644 --- a/grafana-plugin/src/models/escalation_chain/escalation_chain.ts +++ b/grafana-plugin/src/models/escalation_chain/escalation_chain.ts @@ -65,9 +65,9 @@ export class EscalationChainStore extends BaseStore { } @action - async updateItems(query = '', id?: string) { + async updateItems(query = '') { const results = await makeRequest(`${this.path}`, { - params: { search: query, id }, + params: { search: query }, }); this.items = { diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index d9cd7458..e3949832 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -113,7 +113,7 @@ class EscalationChainsPage extends React.Component { const { store } = this.props; - return store.escalationChainStore.updateItems('', this.props.query.id); + return store.escalationChainStore.updateItems(''); }; componentDidUpdate() {} From 4e9ce0ea66c2a7b72cfa96ab2382a61210981940 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Mon, 12 Sep 2022 11:02:00 +0200 Subject: [PATCH 16/89] changes after review --- .../src/containers/DefaultPageLayout/DefaultPageLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index 002beb7c..a8fe366c 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -66,7 +66,7 @@ const DefaultPageLayout: FC = observer((props) => { const { currentUser } = userStore; const isChatOpsConnected = getIfChatOpsConnected(currentUser); - const isPhoneVerified = currentUser?.cloud_connection_status === 3 ? true : currentUser?.verified_phone_number; + const isPhoneVerified = currentUser?.cloud_connection_status === 3 || currentUser?.verified_phone_number; return (
From e4ae10c678cebfb6c8add82c65f630a46013678e Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 12 Sep 2022 17:26:23 +0300 Subject: [PATCH 17/89] Wrong team error for schedules and custom buttons, add flag to get object from the whole organization --- engine/apps/api/tests/test_custom_button.py | 41 ++++++++++++ engine/apps/api/tests/test_schedules.py | 51 +++++++++++++++ engine/apps/api/views/custom_button.py | 19 +++--- engine/apps/api/views/schedule.py | 69 ++++++++++++--------- 4 files changed, 144 insertions(+), 36 deletions(-) diff --git a/engine/apps/api/tests/test_custom_button.py b/engine/apps/api/tests/test_custom_button.py index 3c358c90..bc91fc60 100644 --- a/engine/apps/api/tests/test_custom_button.py +++ b/engine/apps/api/tests/test_custom_button.py @@ -377,3 +377,44 @@ def test_custom_button_action_permissions( response = client.post(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status + + +@pytest.mark.django_db +def test_get_custom_button_from_other_team_with_flag( + make_organization_and_user_with_plugin_token, + make_team, + make_user_auth_headers, + make_custom_action, +): + organization, user, token = make_organization_and_user_with_plugin_token() + + team = make_team(organization) + + custom_button = make_custom_action(organization=organization, team=team) + client = APIClient() + + url = reverse("api-internal:custom_button-detail", kwargs={"pk": custom_button.public_primary_key}) + url = f"{url}?from_organization=true" + + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_custom_button_from_other_team_without_flag( + make_organization_and_user_with_plugin_token, + make_team, + make_user_auth_headers, + make_custom_action, +): + organization, user, token = make_organization_and_user_with_plugin_token() + + team = make_team(organization) + + custom_button = make_custom_action(organization=organization, team=team) + client = APIClient() + + url = reverse("api-internal:custom_button-detail", kwargs={"pk": custom_button.public_primary_key}) + + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index ea3a6dc0..7139f2e9 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -1401,3 +1401,54 @@ def test_schedule_mention_options_permissions( response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status + + +@pytest.mark.django_db +def test_get_schedule_from_other_team_with_flag( + make_organization_and_user_with_plugin_token, + make_team, + make_user_auth_headers, + make_schedule, +): + organization, user, token = make_organization_and_user_with_plugin_token() + + team = make_team(organization) + + calendar_schedule = make_schedule( + organization, + schedule_class=OnCallScheduleCalendar, + name="test_calendar_schedule", + team=team, + ) + + client = APIClient() + url = reverse("api-internal:schedule-detail", kwargs={"pk": calendar_schedule.public_primary_key}) + url = f"{url}?from_organization=true" + + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_get_schedule_from_other_team_without_flag( + make_organization_and_user_with_plugin_token, + make_team, + make_user_auth_headers, + make_schedule, +): + organization, user, token = make_organization_and_user_with_plugin_token() + + team = make_team(organization) + + calendar_schedule = make_schedule( + organization, + schedule_class=OnCallScheduleCalendar, + name="test_calendar_schedule", + team=team, + ) + + client = APIClient() + url = reverse("api-internal:schedule-detail", kwargs={"pk": calendar_schedule.public_primary_key}) + + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/engine/apps/api/views/custom_button.py b/engine/apps/api/views/custom_button.py index 0a9f1973..8d2a8082 100644 --- a/engine/apps/api/views/custom_button.py +++ b/engine/apps/api/views/custom_button.py @@ -12,11 +12,11 @@ from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, from apps.api.serializers.custom_button import CustomButtonSerializer from apps.auth_token.auth import PluginAuthentication from common.api_helpers.exceptions import BadRequest -from common.api_helpers.mixins import PublicPrimaryKeyMixin +from common.api_helpers.mixins import PublicPrimaryKeyMixin, TeamFilteringMixin from common.insight_log import EntityEvent, write_resource_insight_log -class CustomButtonView(PublicPrimaryKeyMixin, ModelViewSet): +class CustomButtonView(TeamFilteringMixin, PublicPrimaryKeyMixin, ModelViewSet): authentication_classes = (PluginAuthentication,) permission_classes = (IsAuthenticated, ActionPermission) action_permissions = { @@ -36,7 +36,15 @@ class CustomButtonView(PublicPrimaryKeyMixin, ModelViewSet): return queryset def get_object(self): - # Override this method because we want to get object from organization instead of concrete team. + # get the object from the whole organization if there is a flag `get_from_organization=true` + # otherwise get the object from the current team + get_from_organization = self.request.query_params.get("from_organization", "false") == "true" + if get_from_organization: + return self.get_object_from_organization() + return super().get_object() + + def get_object_from_organization(self): + # use this method to get the object from the whole organization instead of the current team pk = self.kwargs["pk"] organization = self.request.auth.organization @@ -50,9 +58,6 @@ class CustomButtonView(PublicPrimaryKeyMixin, ModelViewSet): return obj - def original_get_object(self): - return super().get_object() - def perform_create(self, serializer): serializer.save() write_resource_insight_log( @@ -85,7 +90,7 @@ class CustomButtonView(PublicPrimaryKeyMixin, ModelViewSet): def action(self, request, pk): alert_group_id = request.query_params.get("alert_group", None) if alert_group_id is not None: - custom_button = self.original_get_object() + custom_button = self.get_object() try: alert_group = AlertGroup.unarchived_objects.get( public_primary_key=alert_group_id, channel=custom_button.alert_receive_channel diff --git a/engine/apps/api/views/schedule.py b/engine/apps/api/views/schedule.py index ece5186c..4bc5764e 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -32,6 +32,7 @@ from common.api_helpers.mixins import ( CreateSerializerMixin, PublicPrimaryKeyMixin, ShortSerializerMixin, + TeamFilteringMixin, UpdateSerializerMixin, ) from common.api_helpers.utils import create_engine_url, get_date_range_from_request @@ -43,7 +44,12 @@ EVENTS_FILTER_BY_FINAL = "final" class ScheduleView( - PublicPrimaryKeyMixin, ShortSerializerMixin, CreateSerializerMixin, UpdateSerializerMixin, ModelViewSet + TeamFilteringMixin, + PublicPrimaryKeyMixin, + ShortSerializerMixin, + CreateSerializerMixin, + UpdateSerializerMixin, + ModelViewSet, ): authentication_classes = (PluginAuthentication,) permission_classes = (IsAuthenticated, ActionPermission) @@ -124,28 +130,6 @@ class ScheduleView( queryset = self.serializer_class.setup_eager_loading(queryset) return queryset - def get_object(self): - # Override this method because we want to get object from organization instead of concrete team. - pk = self.kwargs["pk"] - organization = self.request.auth.organization - queryset = organization.oncall_schedules.filter( - public_primary_key=pk, - ) - queryset = self._annotate_queryset(queryset) - - try: - obj = queryset.get() - except ObjectDoesNotExist: - raise NotFound - - # May raise a permission denied - self.check_object_permissions(self.request, obj) - - return obj - - def original_get_object(self): - return super().get_object() - def perform_create(self, serializer): serializer.save() write_resource_insight_log(instance=serializer.instance, author=self.request.user, event=EntityEvent.CREATED) @@ -178,6 +162,33 @@ class ScheduleView( if instance.user_group is not None: update_slack_user_group_for_schedules.apply_async((instance.user_group.pk,)) + def get_object(self): + # get the object from the whole organization if there is a flag `get_from_organization=true` + # otherwise get the object from the current team + get_from_organization = self.request.query_params.get("from_organization", "false") == "true" + if get_from_organization: + return self.get_object_from_organization() + return super().get_object() + + def get_object_from_organization(self): + # use this method to get the object from the whole organization instead of the current team + pk = self.kwargs["pk"] + organization = self.request.auth.organization + queryset = organization.oncall_schedules.filter( + public_primary_key=pk, + ) + queryset = self._annotate_queryset(queryset) + + try: + obj = queryset.get() + except ObjectDoesNotExist: + raise NotFound + + # May raise a permission denied + self.check_object_permissions(self.request, obj) + + return obj + def get_request_timezone(self): user_tz = self.request.query_params.get("user_tz", "UTC") try: @@ -203,7 +214,7 @@ class ScheduleView( with_empty = self.request.query_params.get("with_empty", False) == "true" with_gap = self.request.query_params.get("with_gap", False) == "true" - schedule = self.original_get_object() + schedule = self.get_object() events = schedule.filter_events(user_tz, date, days=1, with_empty=with_empty, with_gap=with_gap) slack_channel = ( @@ -235,7 +246,7 @@ class ScheduleView( raise BadRequest(detail="Invalid type value") resolve_schedule = filter_by is None or filter_by == EVENTS_FILTER_BY_FINAL - schedule = self.original_get_object() + schedule = self.get_object() if filter_by is not None and filter_by != EVENTS_FILTER_BY_FINAL: filter_by = OnCallSchedule.PRIMARY if filter_by == EVENTS_FILTER_BY_ROTATION else OnCallSchedule.OVERRIDES @@ -259,7 +270,7 @@ class ScheduleView( user_tz, _ = self.get_request_timezone() now = timezone.now() starting_date = now.date() - schedule = self.original_get_object() + schedule = self.get_object() events = schedule.final_events(user_tz, starting_date, days=30) users = {u: None for u in schedule.related_users()} @@ -274,7 +285,7 @@ class ScheduleView( @action(detail=True, methods=["get"]) def related_escalation_chains(self, request, pk): """Return escalation chains associated to schedule.""" - schedule = self.original_get_object() + schedule = self.get_object() escalation_chains = EscalationChain.objects.filter(escalation_policies__notify_schedule=schedule).distinct() result = [{"name": e.name, "pk": e.public_primary_key} for e in escalation_chains] @@ -290,7 +301,7 @@ class ScheduleView( @action(detail=True, methods=["post"]) def reload_ical(self, request, pk): - schedule = self.original_get_object() + schedule = self.get_object() schedule.drop_cached_ical() schedule.check_empty_shifts_for_next_week() schedule.check_gaps_for_next_week() @@ -302,7 +313,7 @@ class ScheduleView( @action(detail=True, methods=["get", "post", "delete"]) def export_token(self, request, pk): - schedule = self.original_get_object() + schedule = self.get_object() if self.request.method == "GET": try: From 7d5636291a4c7f61db9c77a824ccf1f467a8dac0 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 12 Sep 2022 17:30:45 +0300 Subject: [PATCH 18/89] Wrong team error for integrations and escalation chains pages --- engine/apps/api/tests/test_team.py | 61 +++++++++---------- .../apps/api/views/alert_receive_channel.py | 2 + engine/apps/api/views/escalation_chain.py | 4 +- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/engine/apps/api/tests/test_team.py b/engine/apps/api/tests/test_team.py index aae7360f..53faeca6 100644 --- a/engine/apps/api/tests/test_team.py +++ b/engine/apps/api/tests/test_team.py @@ -3,6 +3,7 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient +from apps.schedules.models import OnCallScheduleCalendar from apps.user_management.models import Team from common.constants.role import Role @@ -114,17 +115,16 @@ def test_team_permissions_wrong_team_general( alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) - # escalation_chain = make_escalation_chain(organization) - # schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) - # webhook = make_custom_action(organization) + escalation_chain = make_escalation_chain(organization) + schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) + webhook = make_custom_action(organization) for endpoint, instance in ( ("alertgroup", alert_group), - # todo: implement team filtering for other resources - # ("alert_receive_channel", alert_receive_channel), - # ("escalation_chain", escalation_chain), - # ("schedule", schedule), - # ("custom_button", webhook), + ("alert_receive_channel", alert_receive_channel), + ("escalation_chain", escalation_chain), + ("schedule", schedule), + ("custom_button", webhook), ): client = APIClient() url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key}) @@ -162,17 +162,16 @@ def test_team_permissions_wrong_team( alert_receive_channel = make_alert_receive_channel(organization, team=team) alert_group = make_alert_group(alert_receive_channel) - # escalation_chain = make_escalation_chain(organization, team=team) - # schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team) - # webhook = make_custom_action(organization, team=team) + escalation_chain = make_escalation_chain(organization, team=team) + schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team) + webhook = make_custom_action(organization, team=team) for endpoint, instance in ( ("alertgroup", alert_group), - # todo: implement team filtering for other resources - # ("alert_receive_channel", alert_receive_channel), - # ("escalation_chain", escalation_chain), - # ("schedule", schedule), - # ("custom_button", webhook), + ("alert_receive_channel", alert_receive_channel), + ("escalation_chain", escalation_chain), + ("schedule", schedule), + ("custom_button", webhook), ): client = APIClient() url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key}) @@ -214,17 +213,16 @@ def test_team_permissions_not_in_team( alert_receive_channel = make_alert_receive_channel(organization, team=team) alert_group = make_alert_group(alert_receive_channel) - # escalation_chain = make_escalation_chain(organization, team=team) - # schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team) - # webhook = make_custom_action(organization, team=team) + escalation_chain = make_escalation_chain(organization, team=team) + schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team) + webhook = make_custom_action(organization, team=team) for endpoint, instance in ( ("alertgroup", alert_group), - # todo: implement team filtering for other resources - # ("alert_receive_channel", alert_receive_channel), - # ("escalation_chain", escalation_chain), - # ("schedule", schedule), - # ("custom_button", webhook), + ("alert_receive_channel", alert_receive_channel), + ("escalation_chain", escalation_chain), + ("schedule", schedule), + ("custom_button", webhook), ): client = APIClient() url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key}) @@ -262,17 +260,16 @@ def test_team_permissions_right_team( alert_receive_channel = make_alert_receive_channel(organization, team=team) alert_group = make_alert_group(alert_receive_channel) - # escalation_chain = make_escalation_chain(organization, team=team) - # schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team) - # webhook = make_custom_action(organization, team=team) + escalation_chain = make_escalation_chain(organization, team=team) + schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team) + webhook = make_custom_action(organization, team=team) for endpoint, instance in ( ("alertgroup", alert_group), - # todo: implement team filtering for other resources - # ("alert_receive_channel", alert_receive_channel), - # ("escalation_chain", escalation_chain), - # ("schedule", schedule), - # ("custom_button", webhook), + ("alert_receive_channel", alert_receive_channel), + ("escalation_chain", escalation_chain), + ("schedule", schedule), + ("custom_button", webhook), ): client = APIClient() url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key}) diff --git a/engine/apps/api/views/alert_receive_channel.py b/engine/apps/api/views/alert_receive_channel.py index 6dce1c56..866620a8 100644 --- a/engine/apps/api/views/alert_receive_channel.py +++ b/engine/apps/api/views/alert_receive_channel.py @@ -22,6 +22,7 @@ from common.api_helpers.mixins import ( FilterSerializerMixin, PreviewTemplateMixin, PublicPrimaryKeyMixin, + TeamFilteringMixin, UpdateSerializerMixin, ) from common.exceptions import TeamCanNotBeChangedError, UnableToSendDemoAlert @@ -58,6 +59,7 @@ class AlertReceiveChannelFilter(filters.FilterSet): class AlertReceiveChannelView( PreviewTemplateMixin, + TeamFilteringMixin, PublicPrimaryKeyMixin, FilterSerializerMixin, UpdateSerializerMixin, diff --git a/engine/apps/api/views/escalation_chain.py b/engine/apps/api/views/escalation_chain.py index f972a992..72c73d3a 100644 --- a/engine/apps/api/views/escalation_chain.py +++ b/engine/apps/api/views/escalation_chain.py @@ -11,11 +11,11 @@ from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, from apps.api.serializers.escalation_chain import EscalationChainListSerializer, EscalationChainSerializer from apps.auth_token.auth import PluginAuthentication from common.api_helpers.exceptions import BadRequest -from common.api_helpers.mixins import ListSerializerMixin, PublicPrimaryKeyMixin +from common.api_helpers.mixins import ListSerializerMixin, PublicPrimaryKeyMixin, TeamFilteringMixin from common.insight_log import EntityEvent, write_resource_insight_log -class EscalationChainViewSet(PublicPrimaryKeyMixin, ListSerializerMixin, viewsets.ModelViewSet): +class EscalationChainViewSet(TeamFilteringMixin, PublicPrimaryKeyMixin, ListSerializerMixin, viewsets.ModelViewSet): authentication_classes = (PluginAuthentication,) permission_classes = (IsAuthenticated, ActionPermission) From f4501311cc69a2d7cc0d6b8ee9419d2b41b4171c Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 14 Sep 2022 13:47:40 +0300 Subject: [PATCH 19/89] reset wrong team boolean on query parse --- grafana-plugin/src/network/index.ts | 21 ------------------- .../escalation-chains/EscalationChains.tsx | 13 ++++++++---- .../src/pages/incident/Incident.tsx | 3 ++- .../src/pages/integrations/Integrations.tsx | 11 ++++++---- .../src/pages/schedules/Schedules.tsx | 12 ++++++----- grafana-plugin/src/pages/users/Users.tsx | 2 ++ 6 files changed, 27 insertions(+), 35 deletions(-) diff --git a/grafana-plugin/src/network/index.ts b/grafana-plugin/src/network/index.ts index 23fbd379..aa35fb3b 100644 --- a/grafana-plugin/src/network/index.ts +++ b/grafana-plugin/src/network/index.ts @@ -30,32 +30,11 @@ interface RequestConfig { validateStatus?: (status: number) => boolean; } -const failPaths = [ - 'api/plugin-proxy/grafana-oncall-app/api/internal/v1/users/URPAN2A31CVWQ/', - 'api/plugin-proxy/grafana-oncall-app/api/internal/v1/escalation_chains/FDF7ZQMNKYIQK/', - 'api/plugin-proxy/grafana-oncall-app/api/internal/v1/alert_receive_channels/CSPE3C7R4Q38G/', -]; - export const makeRequest = async (path: string, config: RequestConfig) => { const { method = 'GET', params, data, validateStatus } = config; const url = `${API_PROXY_PREFIX}${API_PATH_PREFIX}${path}`; - if (failPaths.includes(url)) { - throw { - response: { - status: 403, - data: { - error_code: 'wrong_team', - owner_team: { - name: 'Rares', - id: '14999718', - }, - }, - }, - }; - } - const response = await instance({ method, url, diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index e3949832..c50b7886 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -66,13 +66,14 @@ class EscalationChainsPage extends React.Component { + this.setState({ wrongTeamError: false }); // reset wrong team error to false on query parse + const { store, query } = this.props; + const { escalationChainStore } = store; const { escalationChainsFilters: { searchTerm }, } = this.state; - const { escalationChainStore } = store; - const searchResult = escalationChainStore.getSearchResult(searchTerm); let selectedEscalationChain: EscalationChain['id']; @@ -80,7 +81,9 @@ class EscalationChainsPage extends React.Component this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!escalationChain) {return;} + if (!escalationChain) { + return; + } escalationChain = escalationChainStore.items[query.id]; if (escalationChain) { @@ -91,9 +94,11 @@ class EscalationChainsPage extends React.Component } update = () => { + this.setState({ wrongTeamError: false }); // reset wrong team error to false + const { store, query: { id }, @@ -118,7 +120,6 @@ class IncidentPage extends React.Component } = this.state; const { alertReceiveChannelStore } = store; - const { alerts } = store.alertGroupStore; const incident = alerts.get(id); diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 08d069d9..c9476ae1 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -69,8 +69,9 @@ class Integrations extends React.Component }; parseQueryParams = async () => { - const { store, query } = this.props; + this.setState({ wrongTeamError: false }); // reset wrong team error to false on query parse + const { store, query } = this.props; const { alertReceiveChannelStore } = store; const searchResult = alertReceiveChannelStore.getSearchResult(); @@ -80,7 +81,7 @@ class Integrations extends React.Component let alertReceiveChannel = await alertReceiveChannelStore .loadItem(query.id, true) .catch((error) => this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!alertReceiveChannel) {return;} + if (!alertReceiveChannel) return; if (alertReceiveChannel.id) { selectedAlertReceiveChannel = alertReceiveChannel.id; @@ -95,9 +96,11 @@ class Integrations extends React.Component this.setState({ alertReceiveChannelToShowSettings: query.id }); } } + if (!selectedAlertReceiveChannel) { selectedAlertReceiveChannel = searchResult[0]?.id; } + this.setSelectedAlertReceiveChannel(selectedAlertReceiveChannel); }; @@ -134,8 +137,8 @@ class Integrations extends React.Component if (wrongTeamError) { return ( diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 62e3dfef..ffc994c2 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -83,17 +83,19 @@ class SchedulesPage extends React.Component { + parseQueryParams = async () => { + this.setState({ wrongTeamError: false }); // reset wrong team error to false on query parse + const { store, query: { id }, } = this.props; if (id) { - const schedule = store.scheduleStore + const schedule = await store.scheduleStore .loadItem(id, true) .catch((error) => this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!schedule) {return;} + if (!schedule) return; const schedules = store.scheduleStore.getSearchResult(); const scheduleId = schedules && schedules.find((res) => res.id === id)?.id; @@ -121,8 +123,8 @@ class SchedulesPage extends React.Component diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index 6c9b961f..de013008 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -100,6 +100,8 @@ class Users extends React.Component { } parseParams = async () => { + this.setState({ wrongTeamError: false }); // reset wrong team error to false on query parse + const { store, query: { id }, From 89361ba7e651acc7bf9bb806f4f38b2b32532cb5 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 14 Sep 2022 14:32:14 +0300 Subject: [PATCH 20/89] fix for webhook crash --- .../OutgoingWebhookForm/OutgoingWebhookForm.config.ts | 2 +- .../OutgoingWebhookForm/OutgoingWebhookForm.tsx | 8 ++------ .../src/pages/escalation-chains/EscalationChains.tsx | 6 +++++- .../src/pages/outgoing_webhooks/OutgoingWebhooks.tsx | 5 +++++ 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.ts b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.ts index 76fecf4f..836f1bcf 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.ts +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.ts @@ -34,7 +34,7 @@ export const form: { name: string; fields: FormItem[] } = { }, { name: 'data', - getDisabled: (form_data) => Boolean(form_data.forward_whole_payload), + getDisabled: (form_data) => Boolean(form_data?.forward_whole_payload), type: FormItemType.TextArea, description: 'Available variables: {{ alert_payload }}, {{ alert_group_id }}', extra: { diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx index 8385d0f8..76891c47 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx @@ -1,19 +1,15 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback } from 'react'; -import { Button, Drawer, Input, Modal } from '@grafana/ui'; +import { Button, Drawer } from '@grafana/ui'; import cn from 'classnames/bind'; -import { get } from 'lodash-es'; import { observer } from 'mobx-react'; -import Emoji from 'react-emoji-render'; import GForm from 'components/GForm/GForm'; -import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; import Text from 'components/Text/Text'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; import { useStore } from 'state/useStore'; import { UserAction } from 'state/userAction'; -import { openErrorNotification } from 'utils'; import { form } from './OutgoingWebhookForm.config'; diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index c50b7886..ad713d8d 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -121,7 +121,11 @@ class EscalationChainsPage extends React.Component { + this.setState({ + wrongTeamError: false, + outgoingWebhookIdToEdit: undefined, + }); // reset state on query parse + const { store, query: { id }, From 4dfabb5ead1c71e7e31dbbc71c7e61a9d36cf273 Mon Sep 17 00:00:00 2001 From: "S. M. Mir-Ismaili" Date: Wed, 14 Sep 2022 16:48:32 +0430 Subject: [PATCH 21/89] Increase num of `getPluginSyncStatus` retries to 10 --- grafana-plugin/src/state/rootBaseStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/state/rootBaseStore.ts b/grafana-plugin/src/state/rootBaseStore.ts index e68a701b..7cc26416 100644 --- a/grafana-plugin/src/state/rootBaseStore.ts +++ b/grafana-plugin/src/state/rootBaseStore.ts @@ -219,7 +219,7 @@ export class RootBaseStore { this.handleSyncException(e); }); - if (counter >= 5) { + if (counter >= 10) { clearInterval(interval); this.retrySync = true; } From f2072034d55859022a8f0ee06a7d7b55298728f2 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 14 Sep 2022 16:06:31 +0300 Subject: [PATCH 22/89] linter --- grafana-plugin/src/pages/integrations/Integrations.tsx | 2 +- grafana-plugin/src/pages/schedules/Schedules.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index c9476ae1..47051656 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -81,7 +81,7 @@ class Integrations extends React.Component let alertReceiveChannel = await alertReceiveChannelStore .loadItem(query.id, true) .catch((error) => this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!alertReceiveChannel) return; + if (!alertReceiveChannel) {return;} if (alertReceiveChannel.id) { selectedAlertReceiveChannel = alertReceiveChannel.id; diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index ffc994c2..bc268aeb 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -95,7 +95,7 @@ class SchedulesPage extends React.Component this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!schedule) return; + if (!schedule) {return;} const schedules = store.scheduleStore.getSearchResult(); const scheduleId = schedules && schedules.find((res) => res.id === id)?.id; From 4bcdcfa381ba70aa074e8091fba78f39a253c350 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 16 Sep 2022 11:52:38 +0300 Subject: [PATCH 23/89] Wrong team error for user page --- engine/apps/api/views/user.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index affd0ad9..3062b708 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -23,6 +23,7 @@ from apps.api.permissions import ( IsAdminOrEditor, IsOwnerOrAdmin, ) +from apps.api.serializers.team import TeamSerializer from apps.api.serializers.user import FilterUserSerializer, UserHiddenFieldsSerializer, UserSerializer from apps.auth_token.auth import ( MobileAppAuthTokenAuthentication, @@ -39,7 +40,7 @@ from apps.telegram.client import TelegramClient from apps.telegram.models import TelegramVerificationCode from apps.twilioapp.phone_manager import PhoneManager from apps.twilioapp.twilio_client import twilio_client -from apps.user_management.models import User +from apps.user_management.models import Team, User from common.api_helpers.exceptions import Conflict from common.api_helpers.mixins import FilterSerializerMixin, PublicPrimaryKeyMixin from common.api_helpers.paginators import HundredPageSizePaginator @@ -230,7 +231,10 @@ class UserView( def retrieve(self, request, *args, **kwargs): context = {"request": self.request, "format": self.format_kwarg, "view": self} - instance = self.get_object() + try: + instance = self.get_object() + except NotFound: + return self.wrong_team_response() if settings.OSS_INSTALLATION and live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED: from apps.oss_installation.models import CloudConnector, CloudUserIdentity @@ -245,6 +249,26 @@ class UserView( serializer = self.get_serializer(instance, context=context) return Response(serializer.data) + def wrong_team_response(self): + """ + This method returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}. + Used in case if a requested instance doesn't belong to user's current_team. + """ + queryset = User.objects.filter(organization=self.request.user.organization).order_by("id") + queryset = self.filter_queryset(queryset) + + try: + queryset.get(public_primary_key=self.kwargs["pk"]) + except ObjectDoesNotExist: + raise NotFound + + general_team = Team(public_primary_key=None, name="General", email=None, avatar_url=None) + + return Response( + data={"error_code": "wrong_team", "owner_team": TeamSerializer(general_team).data}, + status=status.HTTP_403_FORBIDDEN, + ) + def current(self, request): serializer = UserSerializer(self.get_queryset().get(pk=self.request.user.pk)) return Response(serializer.data) From c92faf23c6fa99960b32d7aae6cb20429f429065 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 16 Sep 2022 11:53:47 +0300 Subject: [PATCH 24/89] Add tests for wrong team for user --- engine/apps/api/tests/test_team.py | 41 +++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/engine/apps/api/tests/test_team.py b/engine/apps/api/tests/test_team.py index 53faeca6..40df30d8 100644 --- a/engine/apps/api/tests/test_team.py +++ b/engine/apps/api/tests/test_team.py @@ -106,12 +106,16 @@ def test_team_permissions_wrong_team_general( user = make_user(organization=organization) _, token = make_token_for_organization(organization) + client = APIClient() + team = make_team(organization) user.teams.add(team) user.current_team = team user.save(update_fields=["current_team"]) + user_from_general_team = make_user(organization=organization) + alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) @@ -125,8 +129,8 @@ def test_team_permissions_wrong_team_general( ("escalation_chain", escalation_chain), ("schedule", schedule), ("custom_button", webhook), + ("user", user_from_general_team), ): - client = APIClient() url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key}) response = client.get(url, **make_user_auth_headers(user, token)) @@ -156,9 +160,16 @@ def test_team_permissions_wrong_team( user = make_user(organization=organization) _, token = make_token_for_organization(organization) + client = APIClient() + team = make_team(organization) user.teams.add(team) + another_user = make_user(organization=organization) + another_user.teams.add(team) + another_user.current_team = team + another_user.save(update_fields=["current_team"]) + alert_receive_channel = make_alert_receive_channel(organization, team=team) alert_group = make_alert_group(alert_receive_channel) @@ -173,7 +184,6 @@ def test_team_permissions_wrong_team( ("schedule", schedule), ("custom_button", webhook), ): - client = APIClient() url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key}) response = client.get(url, **make_user_auth_headers(user, token)) @@ -189,6 +199,12 @@ def test_team_permissions_wrong_team( }, } + # Every user belongs to General team + url = reverse(f"api-internal:user-detail", kwargs={"pk": another_user.public_primary_key}) + response = client.get(url, **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_200_OK + @pytest.mark.django_db def test_team_permissions_not_in_team( @@ -208,8 +224,15 @@ def test_team_permissions_not_in_team( user = make_user(organization=organization) _, token = make_token_for_organization(organization) + client = APIClient() + team = make_team(organization) + another_user = make_user(organization=organization) + another_user.teams.add(team) + another_user.current_team = team + another_user.save(update_fields=["current_team"]) + alert_receive_channel = make_alert_receive_channel(organization, team=team) alert_group = make_alert_group(alert_receive_channel) @@ -224,7 +247,6 @@ def test_team_permissions_not_in_team( ("schedule", schedule), ("custom_button", webhook), ): - client = APIClient() url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key}) response = client.get(url, **make_user_auth_headers(user, token)) @@ -232,6 +254,12 @@ def test_team_permissions_not_in_team( assert response.status_code == status.HTTP_403_FORBIDDEN assert response.json() == {"error_code": "wrong_team"} + # Every user belongs to General team + url = reverse(f"api-internal:user-detail", kwargs={"pk": another_user.public_primary_key}) + response = client.get(url, **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_200_OK + @pytest.mark.django_db def test_team_permissions_right_team( @@ -251,12 +279,17 @@ def test_team_permissions_right_team( user = make_user(organization=organization) _, token = make_token_for_organization(organization) + client = APIClient() + team = make_team(organization) user.teams.add(team) user.current_team = team user.save(update_fields=["current_team"]) + another_user = make_user(organization=organization) + another_user.teams.add(team) + alert_receive_channel = make_alert_receive_channel(organization, team=team) alert_group = make_alert_group(alert_receive_channel) @@ -270,8 +303,8 @@ def test_team_permissions_right_team( ("escalation_chain", escalation_chain), ("schedule", schedule), ("custom_button", webhook), + ("user", another_user), ): - client = APIClient() url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key}) response = client.get(url, **make_user_auth_headers(user, token)) From d71d8c42c6d361fbfbb453ae7b3665ed17ec1988 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 16 Sep 2022 16:48:05 +0300 Subject: [PATCH 25/89] fetch item with ?from_organization=true --- engine/config_integrations/alertmanager.py | 2 +- engine/config_integrations/grafana_alerting.py | 2 +- .../src/components/Policy/EscalationPolicy.tsx | 16 +++++++--------- .../EscalationChainSteps.tsx | 2 +- .../src/containers/GSelect/GSelect.tsx | 4 +++- grafana-plugin/src/models/base_store.ts | 7 +++++-- .../models/outgoing_webhook/outgoing_webhook.ts | 4 ++-- grafana-plugin/src/models/schedule/schedule.ts | 4 ++-- 8 files changed, 22 insertions(+), 19 deletions(-) diff --git a/engine/config_integrations/alertmanager.py b/engine/config_integrations/alertmanager.py index bfdcff2e..cc356e26 100644 --- a/engine/config_integrations/alertmanager.py +++ b/engine/config_integrations/alertmanager.py @@ -9,7 +9,7 @@ is_able_to_autoresolve = True is_demo_alert_enabled = True description = """ -Alerts from Grafana Alertmanager are automatically routed to this integration." +Alerts from Grafana Alertmanager are automatically routed to this integration. {% for dict_item in grafana_alerting_entities %}
Click
here to open contact point, and diff --git a/engine/config_integrations/grafana_alerting.py b/engine/config_integrations/grafana_alerting.py index e8942b1e..4eac0135 100644 --- a/engine/config_integrations/grafana_alerting.py +++ b/engine/config_integrations/grafana_alerting.py @@ -12,7 +12,7 @@ is_able_to_autoresolve = True is_demo_alert_enabled = True description = """ \ -Alerts from Grafana Alertmanager are automatically routed to this integration." +Alerts from Grafana Alertmanager are automatically routed to this integration. {% for dict_item in grafana_alerting_entities %}
Click here to open contact point, and diff --git a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx index 1d2b5ce1..0a8d609b 100644 --- a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx @@ -14,14 +14,11 @@ import GSelect from 'containers/GSelect/GSelect'; import RemoteSelect from 'containers/RemoteSelect/RemoteSelect'; import UserTooltip from 'containers/UserTooltip/UserTooltip'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; -import { ActionDTO } from 'models/action'; import { prepareEscalationPolicy } from 'models/escalation_policy/escalation_policy.helpers'; import { EscalationPolicy as EscalationPolicyType, EscalationPolicyOption, } from 'models/escalation_policy/escalation_policy.types'; -import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config'; -import { User, UserRole } from 'models/user/user.types'; import { WaitDelay } from 'models/wait_delay'; import { SelectOption } from 'state/types'; import { UserAction } from 'state/userAction'; @@ -272,15 +269,15 @@ export class EscalationPolicy extends React.Component - ); @@ -319,6 +316,7 @@ export class EscalationPolicy extends React.Component ); diff --git a/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx b/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx index 062b9b8d..8f9174aa 100644 --- a/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx +++ b/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx @@ -67,7 +67,7 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => { // const STEP_COLORS = ['#52C41A', '#A0D911', '#FADB14', '#FAAD14', COLOR_RED]; const STEP_COLORS = ['#1A7F4B', '#33cc33', '#ffbf00', '#FF8000', COLOR_RED]; - const { alertReceiveChannelStore, escalationPolicyStore } = store; + const { escalationPolicyStore } = store; const escalationPolicy = escalationPolicyStore.items[escalationPolicyId]; diff --git a/grafana-plugin/src/containers/GSelect/GSelect.tsx b/grafana-plugin/src/containers/GSelect/GSelect.tsx index a41c31ce..07e8c562 100644 --- a/grafana-plugin/src/containers/GSelect/GSelect.tsx +++ b/grafana-plugin/src/containers/GSelect/GSelect.tsx @@ -32,6 +32,7 @@ interface GSelectProps { showWarningIfEmptyValue?: boolean; showError?: boolean; nullItemName?: string; + fromOrganization?: boolean; filterOptions?: (id: any) => boolean; dropdownRender?: (menu: ReactElement) => ReactElement; getOptionLabel?: (item: SelectableValue) => React.ReactNode; @@ -59,6 +60,7 @@ const GSelect = observer((props: GSelectProps) => { showWarningIfEmptyValue = false, getDescription, filterOptions, + fromOrganization, } = props; const store = useStore(); @@ -123,7 +125,7 @@ const GSelect = observer((props: GSelectProps) => { (values as string[]).forEach((value: string) => { if (!isNil(value) && !model.items[value] && model.updateItem) { - model.updateItem(value); + model.updateItem(value, fromOrganization); } }); }, [value]); diff --git a/grafana-plugin/src/models/base_store.ts b/grafana-plugin/src/models/base_store.ts index 03115181..aed5144b 100644 --- a/grafana-plugin/src/models/base_store.ts +++ b/grafana-plugin/src/models/base_store.ts @@ -41,8 +41,11 @@ export default class BaseStore { } @action - async getById(id: string, skipErrorHandling = false) { - return await makeRequest(`${this.path}${id}/`, { + async getById(id: string, skipErrorHandling = false, fromOrganization = false) { + let path = `${this.path}${id}`; + if (fromOrganization) {path = path.concat('?from_organization=true');} + + return await makeRequest(path, { method: 'GET', }).catch((error) => this.onApiError(error, skipErrorHandling)); } diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts index d064fa38..f8a435c7 100644 --- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts +++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts @@ -42,8 +42,8 @@ export class OutgoingWebhookStore extends BaseStore { } @action - async updateItem(id: OutgoingWebhook['id']) { - const response = await this.getById(id); + async updateItem(id: OutgoingWebhook['id'], fromOrganization = false) { + const response = await this.getById(id, false, fromOrganization); this.items = { ...this.items, diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index a2b66973..3cf0e8d1 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -76,9 +76,9 @@ export class ScheduleStore extends BaseStore { }; } - async updateItem(id: Schedule['id']) { + async updateItem(id: Schedule['id'], fromOrganization = false) { if (id) { - const item = await this.getById(id); + const item = await this.getById(id, true, fromOrganization); this.items = { ...this.items, From dd77596447960a181dafaefb8062ff9f52bf6a36 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 16 Sep 2022 17:43:31 +0300 Subject: [PATCH 26/89] handle page refresh when switching teams --- grafana-plugin/src/GrafanaPluginRootPage.tsx | 2 +- .../src/components/PluginLink/PluginLink.tsx | 2 +- .../GrafanaTeamSelect/GrafanaTeamSelect.tsx | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/grafana-plugin/src/GrafanaPluginRootPage.tsx b/grafana-plugin/src/GrafanaPluginRootPage.tsx index 870c1cae..05742afd 100644 --- a/grafana-plugin/src/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/GrafanaPluginRootPage.tsx @@ -135,7 +135,7 @@ export const Root = observer((props: AppRootProps) => { return ( - + ); diff --git a/grafana-plugin/src/components/PluginLink/PluginLink.tsx b/grafana-plugin/src/components/PluginLink/PluginLink.tsx index eef5374e..3a218c2e 100644 --- a/grafana-plugin/src/components/PluginLink/PluginLink.tsx +++ b/grafana-plugin/src/components/PluginLink/PluginLink.tsx @@ -11,7 +11,7 @@ interface PluginLinkProps extends LocationUpdate { disabled?: boolean; className?: string; wrap?: boolean; - children: any + children: any; } const cx = cn.bind(styles); diff --git a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx index 1ca31a27..de290790 100644 --- a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx +++ b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; +import { getLocationSrv } from '@grafana/runtime'; import { HorizontalGroup, Icon, IconButton, Label, Tooltip } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; @@ -18,11 +19,14 @@ import styles from './GrafanaTeamSelect.module.css'; const cx = cn.bind(styles); -interface GrafanaTeamSelectProps {} +interface GrafanaTeamSelectProps { + currentPage: string; +} const GrafanaTeamSelect = observer((props: GrafanaTeamSelectProps) => { const store = useStore(); + const { currentPage } = props; const { userStore, grafanaTeamStore } = store; const grafanaTeams = grafanaTeamStore.getSearchResult(); const user = userStore.currentUser; @@ -33,7 +37,10 @@ const GrafanaTeamSelect = observer((props: GrafanaTeamSelectProps) => { const onTeamChange = async (teamId: GrafanaTeam['id']) => { await userStore.updateCurrentUser({ current_team: teamId }); - window.location.reload(); + + const queryParams = new URLSearchParams(); + queryParams.set('page', currentPage); + window.location.search = queryParams.toString(); }; return document.getElementsByClassName('page-header__inner')[0] From 98dbca82c74ab889831cdf8a00a2823b7661391e Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 19 Sep 2022 13:19:56 +0300 Subject: [PATCH 27/89] cleanup --- .../src/components/NotFoundInTeam/WrongTeamStub.module.css | 4 ---- .../src/components/NotFoundInTeam/WrongTeamStub.tsx | 4 ++-- .../src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx | 5 +---- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.module.css b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.module.css index d158a511..70b01cd0 100644 --- a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.module.css +++ b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.module.css @@ -13,7 +13,3 @@ margin-right: 4px; padding-top: 6px; } - -.return-to-list { - margin-top: 32px; -} diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx index 269c07a8..08a104d0 100644 --- a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx +++ b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; -import { Button, VerticalGroup, Icon } from '@grafana/ui'; +import { Button, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; import PluginLink from 'components/PluginLink/PluginLink'; @@ -60,7 +60,7 @@ const WrongTeamStub: FC = (props) => { Change the team )} - + Or return to the {objectName} list for team {currentTeam} diff --git a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx index de290790..05e356c5 100644 --- a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx +++ b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx @@ -1,13 +1,10 @@ import React from 'react'; -import { SelectableValue } from '@grafana/data'; -import { getLocationSrv } from '@grafana/runtime'; -import { HorizontalGroup, Icon, IconButton, Label, Tooltip } from '@grafana/ui'; +import { Icon, Label, Tooltip } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import ReactDOM from 'react-dom'; -import Avatar from 'components/Avatar/Avatar'; import PluginLink from 'components/PluginLink/PluginLink'; import GSelect from 'containers/GSelect/GSelect'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; From b1ea2b062f6b2912c4c05c5c9138e5ecceb705e9 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 13:19:12 +0300 Subject: [PATCH 28/89] Fix shift update for web schedules --- .../schedules/models/custom_on_call_shift.py | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index c4558b12..225ec544 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -433,6 +433,32 @@ class CustomOnCallShift(models.Model): return return next_event_dt + def get_last_event_date(self, date): + """Get start date of the last event before the chosen date""" + assert date >= self.start, "Chosen date should be later or equal to initial event start date" + + event_ical = self.generate_ical(self.start) + initial_event = Event.from_ical(event_ical) + # take shift interval, not event interval. For rolling_users shift it is not the same. + interval = self.interval or 1 + initial_event["rrule"]["INTERVAL"] = interval + initial_event_start = initial_event["DTSTART"].dt + + last_event = None + # repetitions generate the next event shift according with the recurrence rules + repetitions = UnfoldableCalendar(initial_event).RepeatedEvent( + initial_event, initial_event_start.replace(microsecond=0) + ) + ical_iter = repetitions.__iter__() + for event in ical_iter: + if event.start > date: + break + last_event = event + + last_event_dt = last_event.start if last_event else initial_event_start + + return last_event_dt + @cached_property def event_ical_rules(self): # e.g. {'freq': ['WEEKLY'], 'interval': [2], 'byday': ['MO', 'WE', 'FR'], 'wkst': ['SU']} @@ -498,10 +524,9 @@ class CustomOnCallShift(models.Model): self.rolling_users = result self.save(update_fields=["rolling_users"]) - def get_rotation_user_index(self, date=None): + def get_rotation_user_index(self, date): START_ROTATION_INDEX = 0 - date = timezone.now() if not date else date result = START_ROTATION_INDEX if not self.rolling_users or self.frequency is None: @@ -544,8 +569,9 @@ class CustomOnCallShift(models.Model): return last_shift def create_or_update_last_shift(self, data): + now = timezone.now().replace(microsecond=0) # rotation start date cannot be earlier than now - data["rotation_start"] = max(data["rotation_start"], timezone.now().replace(microsecond=0)) + data["rotation_start"] = max(data["rotation_start"], now) # prepare dict with params of existing instance with last updates and remove unique and m2m fields from it shift_to_update = self.last_updated_shift or self instance_data = model_to_dict(shift_to_update) @@ -557,12 +583,10 @@ class CustomOnCallShift(models.Model): instance_data["schedule"] = self.schedule instance_data["team"] = self.team # set new event start date to keep rotation index - instance_data["start"] = timezone.datetime.combine( - instance_data["rotation_start"].date(), - instance_data["start"].time(), - ).astimezone(pytz.UTC) + if instance_data["start"] == self.start: + instance_data["start"] = self.get_last_event_date(now) # calculate rotation index to keep user rotation order - start_rotation_from_user_index = self.get_rotation_user_index() + (self.start_rotation_from_user_index or 0) + start_rotation_from_user_index = self.get_rotation_user_index(now) + (self.start_rotation_from_user_index or 0) if start_rotation_from_user_index >= len(instance_data["rolling_users"]): start_rotation_from_user_index = 0 instance_data["start_rotation_from_user_index"] = start_rotation_from_user_index From d9609dbcc251ccc25c9b2f0fd85da58d9d302be0 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 13:20:28 +0300 Subject: [PATCH 29/89] Fix priority level regex, fix getting shifts without duration --- engine/apps/schedules/constants.py | 2 +- engine/apps/schedules/ical_utils.py | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/engine/apps/schedules/constants.py b/engine/apps/schedules/constants.py index 719aa0b2..a2ec8adc 100644 --- a/engine/apps/schedules/constants.py +++ b/engine/apps/schedules/constants.py @@ -9,6 +9,6 @@ ICAL_ATTENDEE = "ATTENDEE" ICAL_UID = "UID" ICAL_RRULE = "RRULE" ICAL_UNTIL = "UNTIL" -RE_PRIORITY = re.compile(r"^\[L(\d)\]") +RE_PRIORITY = re.compile(r"^\[L(\d+)\]") RE_EVENT_UID_V1 = re.compile(r"amixr-([\w\d-]+)-U(\d+)-E(\d+)-S(\d+)") RE_EVENT_UID_V2 = re.compile(r"oncall-([\w\d-]+)-PK([\w\d]+)-U(\d+)-E(\d+)-S(\d+)") diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index 12fb5bd1..620e4450 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -190,18 +190,19 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ ) else: start, end = ical_events.get_start_and_end_with_respect_to_event_type(event) - result_datetime.append( - { - "start": start.astimezone(pytz.UTC), - "end": end.astimezone(pytz.UTC), - "users": users, - "missing_users": missing_users, - "priority": priority, - "source": source, - "calendar_type": calendar_type, - "shift_pk": pk, - } - ) + if start < end: + result_datetime.append( + { + "start": start.astimezone(pytz.UTC), + "end": end.astimezone(pytz.UTC), + "users": users, + "missing_users": missing_users, + "priority": priority, + "source": source, + "calendar_type": calendar_type, + "shift_pk": pk, + } + ) return result_datetime, result_date From 9b9470b358ab3a631829f1f9ff136254472c7b80 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 13:33:33 +0300 Subject: [PATCH 30/89] Fix shift update for web schedules --- engine/apps/schedules/models/custom_on_call_shift.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 225ec544..232c824a 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -441,7 +441,9 @@ class CustomOnCallShift(models.Model): initial_event = Event.from_ical(event_ical) # take shift interval, not event interval. For rolling_users shift it is not the same. interval = self.interval or 1 - initial_event["rrule"]["INTERVAL"] = interval + if "rrule" in initial_event: + # means that shift has frequency + initial_event["rrule"]["INTERVAL"] = interval initial_event_start = initial_event["DTSTART"].dt last_event = None From 7571bfa62521ed0d419baf29a691355a59c48524 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 14:13:47 +0300 Subject: [PATCH 31/89] Fix tests for shift update --- engine/apps/api/tests/test_oncall_shift.py | 28 ++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index ddd21498..efa2fb96 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -412,8 +412,9 @@ def test_update_old_on_call_shift_with_future_version( token, user1, user2, organization, schedule = on_call_shift_internal_api_setup client = APIClient() - start_date = (timezone.now() - timezone.timedelta(days=3)).replace(microsecond=0) - next_rotation_start_date = start_date + timezone.timedelta(days=5) + now = timezone.now().replace(microsecond=0) + start_date = now - timezone.timedelta(days=3) + next_rotation_start_date = now + timezone.timedelta(days=1) updated_duration = timezone.timedelta(hours=4) title = "Test Shift Rotation" @@ -422,10 +423,11 @@ def test_update_old_on_call_shift_with_future_version( shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, title=title, - start=start_date, + start=next_rotation_start_date, duration=timezone.timedelta(hours=3), rotation_start=next_rotation_start_date, rolling_users=[{user1.pk: user1.public_primary_key}], + frequency=CustomOnCallShift.FREQUENCY_DAILY, ) old_on_call_shift = make_on_call_shift( schedule.organization, @@ -438,6 +440,7 @@ def test_update_old_on_call_shift_with_future_version( until=next_rotation_start_date, rolling_users=[{user1.pk: user1.public_primary_key}], updated_shift=new_on_call_shift, + frequency=CustomOnCallShift.FREQUENCY_DAILY, ) # update shift_end and priority_level data_to_update = { @@ -445,9 +448,9 @@ def test_update_old_on_call_shift_with_future_version( "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + updated_duration).strftime("%Y-%m-%dT%H:%M:%SZ"), - "rotation_start": next_rotation_start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), + "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, - "frequency": None, + "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key]], @@ -461,27 +464,28 @@ def test_update_old_on_call_shift_with_future_version( url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": old_on_call_shift.public_primary_key}) response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) + response_data = response.json() - next_shift_start_date = timezone.datetime.combine(next_rotation_start_date.date(), start_date.time()).astimezone( - timezone.pytz.UTC - ) + for key in ["shift_start", "shift_end", "rotation_start"]: + data_to_update.pop(key) + response_data.pop(key) expected_payload = data_to_update | { "id": new_on_call_shift.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "updated_shift": None, - "shift_start": next_shift_start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), - "shift_end": (next_shift_start_date + updated_duration).strftime("%Y-%m-%dT%H:%M:%SZ"), } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload - new_on_call_shift.refresh_from_db() - # check if the newest version of shift was changed assert old_on_call_shift.duration != updated_duration assert old_on_call_shift.priority_level != data_to_update["priority_level"] + new_on_call_shift.refresh_from_db() + # check if the newest version of shift was changed + assert new_on_call_shift.start - now < timezone.timedelta(minutes=1) + assert new_on_call_shift.rotation_start - now < timezone.timedelta(minutes=1) assert new_on_call_shift.duration == updated_duration assert new_on_call_shift.priority_level == data_to_update["priority_level"] From edba707b42ad6a0159035a81ce4c4236df1f219a Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 14:19:32 +0300 Subject: [PATCH 32/89] Fix priority level test --- engine/apps/slack/tests/test_parse_slack_usernames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/apps/slack/tests/test_parse_slack_usernames.py b/engine/apps/slack/tests/test_parse_slack_usernames.py index df43c950..659cc27a 100644 --- a/engine/apps/slack/tests/test_parse_slack_usernames.py +++ b/engine/apps/slack/tests/test_parse_slack_usernames.py @@ -52,5 +52,5 @@ def test_remove_priority_from_username(): assert parse_username_from_string("[L1] bob") == "bob" assert parse_username_from_string(" [L1] bob ") == "bob" assert parse_username_from_string("[L2] bob[L1]") == "bob[L1]" - assert parse_username_from_string("[L27]bob") == "[L27]bob" + assert parse_username_from_string("[L27]bob") == "bob" assert parse_username_from_string("[[L2]] bob[[[L1]") == "[[L2]] bob[[[L1]" From aeb8de96a282f3b95926eae579fff88eeb45c526 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 14:47:42 +0300 Subject: [PATCH 33/89] Update comment for wrong_team_response in UserView --- engine/apps/api/views/user.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index 3062b708..4d63089c 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -253,6 +253,8 @@ class UserView( """ This method returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}. Used in case if a requested instance doesn't belong to user's current_team. + Used instead of TeamFilteringMixin because of m2m teams field (mixin doesn't work correctly with this) + and overridden retrieve method in UserView. """ queryset = User.objects.filter(organization=self.request.user.organization).order_by("id") queryset = self.filter_queryset(queryset) From 996e6076ab84376eb7a9911c7419c53e76ffa2b7 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 16:42:58 +0300 Subject: [PATCH 34/89] Remove unnecessary variable --- engine/apps/schedules/models/custom_on_call_shift.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 232c824a..3c7c41e9 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -411,8 +411,7 @@ class CustomOnCallShift(models.Model): repetitions = UnfoldableCalendar(current_event).RepeatedEvent( current_event, next_event_start.replace(microsecond=0) ) - ical_iter = repetitions.__iter__() - for event in ical_iter: + for event in repetitions.__iter__(): if end_date: # end_date exists for long events with frequency weekly and monthly if end_date >= event.start >= next_event_start: if ( @@ -451,8 +450,7 @@ class CustomOnCallShift(models.Model): repetitions = UnfoldableCalendar(initial_event).RepeatedEvent( initial_event, initial_event_start.replace(microsecond=0) ) - ical_iter = repetitions.__iter__() - for event in ical_iter: + for event in repetitions.__iter__(): if event.start > date: break last_event = event From 1e4353966309fcbf5a25148060325a4c2dda4384 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 20 Sep 2022 17:24:20 +0300 Subject: [PATCH 35/89] refactored the duplicate code of wrong data state --- .../NotFoundInTeam/WrongTeam.helpers.tsx | 23 -- .../WrongTeamDisplayWrapper.helpers.tsx | 18 ++ ...css => WrongTeamDisplayWrapper.module.css} | 0 ...amStub.tsx => WrongTeamDisplayWrapper.tsx} | 37 ++- grafana-plugin/src/models/base_store.ts | 6 +- .../escalation-chains/EscalationChains.tsx | 177 ++++++------- .../src/pages/incident/Incident.tsx | 161 ++++++------ .../src/pages/integrations/Integrations.tsx | 244 +++++++++--------- .../outgoing_webhooks/OutgoingWebhooks.tsx | 101 ++++---- .../src/pages/schedules/Schedules.tsx | 227 ++++++++-------- grafana-plugin/src/pages/users/Users.tsx | 155 ++++++----- 11 files changed, 546 insertions(+), 603 deletions(-) delete mode 100644 grafana-plugin/src/components/NotFoundInTeam/WrongTeam.helpers.tsx create mode 100644 grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers.tsx rename grafana-plugin/src/components/NotFoundInTeam/{WrongTeamStub.module.css => WrongTeamDisplayWrapper.module.css} (100%) rename grafana-plugin/src/components/NotFoundInTeam/{WrongTeamStub.tsx => WrongTeamDisplayWrapper.tsx} (78%) diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeam.helpers.tsx b/grafana-plugin/src/components/NotFoundInTeam/WrongTeam.helpers.tsx deleted file mode 100644 index 176cbf29..00000000 --- a/grafana-plugin/src/components/NotFoundInTeam/WrongTeam.helpers.tsx +++ /dev/null @@ -1,23 +0,0 @@ -interface WrongTeamResponse { - notFound?: boolean; - wrongTeamError?: boolean; - teamToSwitch?: { name: string; id: string }; - wrongTeamNoPermissions?: boolean; -} - -export function getWrongTeamResponseInfo({ response }): WrongTeamResponse { - if (response) { - if (response.status === 404) { - return { notFound: true }; - } else if (response.status === 403 && response.data.error_code === 'wrong_team') { - let res = response.data; - if (res.owner_team) { - return { wrongTeamError: true, teamToSwitch: { name: res.owner_team.name, id: res.owner_team.id } }; - } else { - return { wrongTeamError: true, wrongTeamNoPermissions: true }; - } - } - } - - return { notFound: true }; -} diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers.tsx b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers.tsx new file mode 100644 index 00000000..71338188 --- /dev/null +++ b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers.tsx @@ -0,0 +1,18 @@ +import { WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; + +export function getWrongTeamResponseInfo({ response }): Partial { + if (response) { + if (response.status === 404) { + return { notFound: true }; + } else if (response.status === 403 && response.data.error_code === 'wrong_team') { + let res = response.data; + if (res.owner_team) { + return { isError: true, switchToTeam: { name: res.owner_team.name, id: res.owner_team.id } }; + } else { + return { isError: true, wrongTeamNoPermissions: true }; + } + } + } + + return { notFound: true }; +} diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.module.css b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.module.css similarity index 100% rename from grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.module.css rename to grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.module.css diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.tsx similarity index 78% rename from grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx rename to grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.tsx index 08a104d0..b940b69b 100644 --- a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamStub.tsx +++ b/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React from 'react'; import { Button, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; @@ -9,25 +9,40 @@ import { ChangeTeamIcon } from 'icons'; import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; import { useStore } from 'state/useStore'; -import styles from './WrongTeamStub.module.css'; +import styles from './WrongTeamWrapperDisplay.module.css'; const cx = cn.bind(styles); -export interface WrongTeamStubProps { - className?: string; - objectName: string; - pageName: string; - switchToTeam?: { name: string; id: string }; +export interface WrongTeamData { + notFound?: boolean; + isError?: boolean; wrongTeamNoPermissions?: boolean; + switchToTeam?: { name: string; id: string }; } -const WrongTeamStub: FC = (props) => { +export function initWrongTeamDataState(): Partial { + return { isError: false, wrongTeamNoPermissions: false }; +} + +export default function WrongTeamDisplayWrapper({ + wrongTeamData, + objectName, + pageName, + children, +}: { + wrongTeamData: WrongTeamData; + objectName: string; + pageName: string; + children: any; +}) { + if (!wrongTeamData.isError) return children(); + const store = useStore(); const currentTeamId = store.userStore.currentUser?.current_team; const currentTeam = store.grafanaTeamStore.items[currentTeamId]?.name; - const { objectName, pageName, switchToTeam, wrongTeamNoPermissions } = props; + const { switchToTeam, wrongTeamNoPermissions } = wrongTeamData; const onTeamChange = async (teamId: GrafanaTeam['id']) => { await store.userStore.updateCurrentUser({ current_team: teamId }); @@ -66,6 +81,4 @@ const WrongTeamStub: FC = (props) => {
); -}; - -export default WrongTeamStub; +} diff --git a/grafana-plugin/src/models/base_store.ts b/grafana-plugin/src/models/base_store.ts index aed5144b..f20c4d6b 100644 --- a/grafana-plugin/src/models/base_store.ts +++ b/grafana-plugin/src/models/base_store.ts @@ -42,11 +42,9 @@ export default class BaseStore { @action async getById(id: string, skipErrorHandling = false, fromOrganization = false) { - let path = `${this.path}${id}`; - if (fromOrganization) {path = path.concat('?from_organization=true');} - - return await makeRequest(path, { + return await makeRequest(`${this.path}${id}`, { method: 'GET', + params: { from_organization: fromOrganization }, }).catch((error) => this.onApiError(error, skipErrorHandling)); } diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index ad713d8d..fac11f4b 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -11,8 +11,8 @@ import Collapse from 'components/Collapse/Collapse'; import EscalationsFilters from 'components/EscalationsFilters/EscalationsFilters'; import Block from 'components/GBlock/Block'; import GList from 'components/GList/GList'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; @@ -39,11 +39,7 @@ interface EscalationChainsPageState { showCreateEscalationChainModal: boolean; escalationChainIdToCopy: EscalationChain['id']; selectedEscalationChain: EscalationChain['id']; - - notFound?: boolean; - wrongTeamError?: boolean; - teamToSwitch?: { name: string; id: string }; - wrongTeamNoPermissions?: boolean; + wrongTeamData: WrongTeamData; } export interface Filters { @@ -57,8 +53,7 @@ class EscalationChainsPage extends React.Component { - this.setState({ wrongTeamError: false }); // reset wrong team error to false on query parse + this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse const { store, query } = this.props; const { escalationChainStore } = store; @@ -80,7 +75,8 @@ class EscalationChainsPage extends React.Component this.setState({ ...getWrongTeamResponseInfo(error) })); + .catch((error) => this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); + if (!escalationChain) { return; } @@ -134,99 +130,90 @@ class EscalationChainsPage extends React.Component - ); - } - const { escalationChainStore } = store; const searchResult = escalationChainStore.getSearchResult(escalationChainsFilters.searchTerm); return ( - <> -
-
- -
- {!searchResult || searchResult.length ? ( -
-
- - - -
- {searchResult ? ( - - {(item) => } - - ) : ( - - )} -
+ + {() => ( + <> +
+
+
-
{this.renderEscalation()}
+ {!searchResult || searchResult.length ? ( +
+
+ + + +
+ {searchResult ? ( + + {(item) => } + + ) : ( + + )} +
+
+
{this.renderEscalation()}
+
+ ) : ( + + No escalations found, check your filtering and current team. + + + + + } + /> + )}
- ) : ( - - No escalations found, check your filtering and current team. - - - - - } - /> - )} -
- {showCreateEscalationChainModal && ( - { - this.setState({ - showCreateEscalationChainModal: false, - escalationChainIdToCopy: undefined, - }); - }} - onUpdate={this.handleEscalationChainCreate} - /> + {showCreateEscalationChainModal && ( + { + this.setState({ + showCreateEscalationChainModal: false, + escalationChainIdToCopy: undefined, + }); + }} + onUpdate={this.handleEscalationChainCreate} + /> + )} + )} - + ); } diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index cfb80afc..006f2580 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -26,8 +26,8 @@ import reactStringReplace from 'react-string-replace'; import Collapse from 'components/Collapse/Collapse'; import Block from 'components/GBlock/Block'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; @@ -62,12 +62,9 @@ interface IncidentPageProps extends WithStoreProps, AppRootProps {} interface IncidentPageState { showIntegrationSettings?: boolean; showAttachIncidentForm?: boolean; - notFound?: boolean; - wrongTeamError?: boolean; - wrongTeamNoPermissions?: boolean; - teamToSwitch?: { name: string; id: string }; timelineFilter: string; resolutionNoteText: string; + wrongTeamData: WrongTeamData; } @observer @@ -75,8 +72,7 @@ class IncidentPage extends React.Component state: IncidentPageState = { timelineFilter: 'all', resolutionNoteText: '', - wrongTeamError: false, - wrongTeamNoPermissions: false, + wrongTeamData: initWrongTeamDataState(), }; componentDidMount() { @@ -94,14 +90,16 @@ class IncidentPage extends React.Component } update = () => { - this.setState({ wrongTeamError: false }); // reset wrong team error to false + this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse // reset wrong team error to false const { store, query: { id }, } = this.props; - store.alertGroupStore.getAlert(id).catch((error) => this.setState({ ...getWrongTeamResponseInfo(error) })); + store.alertGroupStore + .getAlert(id) + .catch((error) => this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); }; render() { @@ -110,50 +108,32 @@ class IncidentPage extends React.Component query: { id, cursor, start, perpage }, } = this.props; - const { - showIntegrationSettings, - showAttachIncidentForm, - notFound, - wrongTeamError, - teamToSwitch, - wrongTeamNoPermissions, - } = this.state; + const { wrongTeamData, showIntegrationSettings, showAttachIncidentForm } = this.state; const { alertReceiveChannelStore } = store; const { alerts } = store.alertGroupStore; const incident = alerts.get(id); - if (notFound) { - return ( -
-
- - 404 - Incident not found - - - - -
-
- ); - } + // if (notFound) { + // return ( + //
+ //
+ // + // 404 + // Incident not found + // + // + // + // + //
+ //
+ // ); + // } - if (wrongTeamError) { - return ( - - ); - } - - if (!incident) { + if (!incident && !wrongTeamData.isError) { return (
@@ -162,48 +142,55 @@ class IncidentPage extends React.Component } return ( - <> -
- {this.renderHeader()} -
-
- - - + + {() => ( + <> +
+ {this.renderHeader()} +
+
+ + + +
+
{this.renderTimeline()}
+
-
{this.renderTimeline()}
-
-
- {showIntegrationSettings && ( - { - alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id); - }} - onUpdateTemplates={() => { - store.alertGroupStore.getAlert(id); - }} - startTab={IntegrationSettingsTab.Templates} - id={incident.alert_receive_channel.id} - onHide={() => - this.setState({ - showIntegrationSettings: undefined, - }) - } - /> + {showIntegrationSettings && ( + { + alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id); + }} + onUpdateTemplates={() => { + store.alertGroupStore.getAlert(id); + }} + startTab={IntegrationSettingsTab.Templates} + id={incident.alert_receive_channel.id} + onHide={() => + this.setState({ + showIntegrationSettings: undefined, + }) + } + /> + )} + {showAttachIncidentForm && ( + { + this.setState({ + showAttachIncidentForm: false, + }); + }} + onUpdate={this.update} + /> + )} + )} - {showAttachIncidentForm && ( - { - this.setState({ - showAttachIncidentForm: false, - }); - }} - onUpdate={this.update} - /> - )} - + ); } diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 47051656..8251143f 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -9,8 +9,8 @@ import { observer } from 'mobx-react'; import GList from 'components/GList/GList'; import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/IntegrationsFilters'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; @@ -36,11 +36,7 @@ interface IntegrationsState { showCreateIntegrationModal: boolean; alertReceiveChannelToShowSettings?: AlertReceiveChannel['id']; integrationSettingsTab?: IntegrationSettingsTab; - - notFound?: boolean; - wrongTeamError?: boolean; - teamToSwitch?: { name: string; id: string }; - wrongTeamNoPermissions?: boolean; + wrongTeamData: WrongTeamData; } interface IntegrationsProps extends WithStoreProps, AppRootProps {} @@ -50,9 +46,7 @@ class Integrations extends React.Component state: IntegrationsState = { integrationsFilters: { searchTerm: '' }, showCreateIntegrationModal: false, - - wrongTeamError: false, - wrongTeamNoPermissions: false, + wrongTeamData: initWrongTeamDataState(), }; alertReceiveChanneltoPoll: { [key: string]: number } = {}; @@ -69,7 +63,7 @@ class Integrations extends React.Component }; parseQueryParams = async () => { - this.setState({ wrongTeamError: false }); // reset wrong team error to false on query parse + this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse // reset wrong team error to false const { store, query } = this.props; const { alertReceiveChannelStore } = store; @@ -80,8 +74,11 @@ class Integrations extends React.Component if (query.id) { let alertReceiveChannel = await alertReceiveChannelStore .loadItem(query.id, true) - .catch((error) => this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!alertReceiveChannel) {return;} + .catch((error) => this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); + + if (!alertReceiveChannel) { + return; + } if (alertReceiveChannel.id) { selectedAlertReceiveChannel = alertReceiveChannel.id; @@ -129,133 +126,124 @@ class Integrations extends React.Component alertReceiveChannelToShowSettings, integrationSettingsTab, showCreateIntegrationModal, - wrongTeamError, - teamToSwitch, - wrongTeamNoPermissions, + wrongTeamData, } = this.state; - if (wrongTeamError) { - return ( - - ); - } - const { alertReceiveChannelStore } = store; const searchResult = alertReceiveChannelStore.getSearchResult(); return ( - <> -
-
- -
- {searchResult?.length ? ( -
-
- - - -
- - {(item) => ( - { - this.setState({ - alertReceiveChannelToShowSettings: item.id, - integrationSettingsTab: IntegrationSettingsTab.Heartbeat, - }); - }} - /> - )} - -
+ + {() => ( + <> +
+
+
-
- { - this.setState({ - alertReceiveChannelToShowSettings: store.selectedAlertReceiveChannel, - integrationSettingsTab, - }); - }} - /*onEditAlertReceiveChannelTemplates={this.getShowAlertReceiveChannelSettingsClickHandler( + {searchResult?.length ? ( +
+
+ + + +
+ + {(item) => ( + { + this.setState({ + alertReceiveChannelToShowSettings: item.id, + integrationSettingsTab: IntegrationSettingsTab.Heartbeat, + }); + }} + /> + )} + +
+
+
+ { + this.setState({ + alertReceiveChannelToShowSettings: store.selectedAlertReceiveChannel, + integrationSettingsTab, + }); + }} + /*onEditAlertReceiveChannelTemplates={this.getShowAlertReceiveChannelSettingsClickHandler( store.selectedAlertReceiveChannel )}*/ + /> +
+
+ ) : searchResult ? ( + + No integrations found. Review your filter and team settings. + + + + + } /> -
+ ) : ( + + )}
- ) : searchResult ? ( - - No integrations found. Review your filter and team settings. - - - - - } - /> - ) : ( - - )} -
- {alertReceiveChannelToShowSettings && ( - { - alertReceiveChannelStore.updateItem(alertReceiveChannelToShowSettings); - }} - startTab={integrationSettingsTab} - id={alertReceiveChannelToShowSettings} - onHide={() => { - this.setState({ - alertReceiveChannelToShowSettings: undefined, - integrationSettingsTab: undefined, - }); - getLocationSrv().update({ partial: true, query: { tab: undefined } }); - }} - /> + {alertReceiveChannelToShowSettings && ( + { + alertReceiveChannelStore.updateItem(alertReceiveChannelToShowSettings); + }} + startTab={integrationSettingsTab} + id={alertReceiveChannelToShowSettings} + onHide={() => { + this.setState({ + alertReceiveChannelToShowSettings: undefined, + integrationSettingsTab: undefined, + }); + getLocationSrv().update({ partial: true, query: { tab: undefined } }); + }} + /> + )} + {showCreateIntegrationModal && ( + { + this.setState({ showCreateIntegrationModal: false }); + }} + onCreate={this.handleCreateNewAlertReceiveChannel} + /> + )} + )} - {showCreateIntegrationModal && ( - { - this.setState({ showCreateIntegrationModal: false }); - }} - onCreate={this.handleCreateNewAlertReceiveChannel} - /> - )} - + ); } diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 94ce682f..ed1221e1 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -7,17 +7,15 @@ import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import GTable from 'components/GTable/GTable'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import WithConfirm from 'components/WithConfirm/WithConfirm'; -import GSelect from 'containers/GSelect/GSelect'; import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookForm'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { ActionDTO } from 'models/action'; import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; -import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config'; import { WithStoreProps } from 'state/types'; import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; @@ -30,17 +28,13 @@ interface OutgoingWebhooksProps extends WithStoreProps, AppRootProps {} interface OutgoingWebhooksState { outgoingWebhookIdToEdit?: OutgoingWebhook['id'] | 'new'; - notFound?: boolean; - wrongTeamError?: boolean; - teamToSwitch?: { name: string; id: string }; - wrongTeamNoPermissions?: boolean; + wrongTeamData: WrongTeamData; } @observer class OutgoingWebhooks extends React.Component { state: OutgoingWebhooksState = { - wrongTeamError: false, - wrongTeamNoPermissions: false, + wrongTeamData: initWrongTeamDataState(), }; async componentDidMount() { @@ -54,10 +48,10 @@ class OutgoingWebhooks extends React.Component { - this.setState({ - wrongTeamError: false, + this.setState((prevState) => ({ + wrongTeamData: initWrongTeamDataState(), outgoingWebhookIdToEdit: undefined, - }); // reset state on query parse + })); // reset state on query parse const { store, @@ -66,7 +60,7 @@ class OutgoingWebhooks extends React.Component this.setState({ ...getWrongTeamResponseInfo(error) })); + .catch((error) => this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); if (id) { this.setState({ outgoingWebhookIdToEdit: id }); @@ -81,18 +75,7 @@ class OutgoingWebhooks extends React.Component - ); - } + const { outgoingWebhookIdToEdit, wrongTeamData } = this.state; const webhooks = store.outgoingWebhookStore.getSearchResult(); @@ -115,39 +98,43 @@ class OutgoingWebhooks extends React.Component -
- ( -
- Outgoing Webhooks - - - - - -
+ + {() => ( + <> +
+ ( +
+ Outgoing Webhooks + + + + + +
+ )} + rowKey="id" + columns={columns} + data={webhooks} + /> +
+ {outgoingWebhookIdToEdit && ( + )} - rowKey="id" - columns={columns} - data={webhooks} - /> -
- {outgoingWebhookIdToEdit && ( - + )} - + ); } diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index bc268aeb..5402a3e4 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -22,8 +22,8 @@ import moment, { Moment } from 'moment-timezone'; import instructionsImage from 'assets/img/events_instructions.png'; import Avatar from 'components/Avatar/Avatar'; import GTable from 'components/GTable/GTable'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import SchedulesFilters from 'components/SchedulesFilters/SchedulesFilters'; import { SchedulesFiltersType } from 'components/SchedulesFilters/SchedulesFilters.types'; @@ -55,11 +55,7 @@ interface SchedulesPageState { scheduleIdToExport?: Schedule['id']; filters: SchedulesFiltersType; expandedSchedulesKeys: Array; - - notFound?: boolean; - wrongTeamError?: boolean; - teamToSwitch?: { name: string; id: string }; - wrongTeamNoPermissions?: boolean; + wrongTeamData: WrongTeamData; } @observer @@ -69,8 +65,7 @@ class SchedulesPage extends React.Component { - this.setState({ wrongTeamError: false }); // reset wrong team error to false on query parse + this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse const { store, @@ -94,8 +89,10 @@ class SchedulesPage extends React.Component this.setState({ ...getWrongTeamResponseInfo(error) })); - if (!schedule) {return;} + .catch((error) => this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); + if (!schedule) { + return; + } const schedules = store.scheduleStore.getSearchResult(); const scheduleId = schedules && schedules.find((res) => res.id === id)?.id; @@ -117,20 +114,9 @@ class SchedulesPage extends React.Component - ); - } - const columns = [ { width: '10%', @@ -176,102 +162,107 @@ class SchedulesPage extends React.Component -
-
- - On-call Schedules - - Use this to distribute notifications among team members you specified in the "Notify Users from on-call - schedule" step in escalation chains. - - -
- {!schedules || schedules.length ? ( - ( -
- - - - Your timezone is {timezoneStr} UTC{offset} - - - - - - - -
+ + {() => ( + <> +
+
+ + On-call Schedules + + Use this to distribute notifications among team members you specified in the "Notify Users from + on-call schedule" step in{' '} + escalation chains. + + +
+ {!schedules || schedules.length ? ( + ( +
+ + + + Your timezone is {timezoneStr} UTC{offset} + + + + + + + +
+ )} + rowKey="id" + columns={columns} + data={schedules} + expandable={{ + expandedRowRender: this.renderEvents, + expandRowByClick: true, + onExpand: this.onRowExpand, + expandedRowKeys: expandedSchedulesKeys, + onExpandedRowsChange: this.handleExpandedRowsChange, + }} + /> + ) : ( + + You haven’t added a schedule yet. + + + + + } + /> )} - rowKey="id" - columns={columns} - data={schedules} - expandable={{ - expandedRowRender: this.renderEvents, - expandRowByClick: true, - onExpand: this.onRowExpand, - expandedRowKeys: expandedSchedulesKeys, - onExpandedRowsChange: this.handleExpandedRowsChange, - }} - /> - ) : ( - - You haven’t added a schedule yet. - - - - - } - /> - )} -
- {scheduleIdToEdit && ( - { - this.setState({ scheduleIdToEdit: undefined }); - getLocationSrv().update({ partial: true, query: { id: undefined } }); - }} - /> +
+ {scheduleIdToEdit && ( + { + this.setState({ scheduleIdToEdit: undefined }); + getLocationSrv().update({ partial: true, query: { id: undefined } }); + }} + /> + )} + {scheduleIdToDelete && ( + { + this.setState({ scheduleIdToDelete: undefined }); + }} + /> + )} + {scheduleIdToExport && ( + this.setState({ scheduleIdToExport: undefined })} + > + + + )} + )} - {scheduleIdToDelete && ( - { - this.setState({ scheduleIdToDelete: undefined }); - }} - /> - )} - {scheduleIdToExport && ( - this.setState({ scheduleIdToExport: undefined })} - > - - - )} - + ); } diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index de013008..d971d4b8 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -9,8 +9,8 @@ import { observer } from 'mobx-react'; import Avatar from 'components/Avatar/Avatar'; import GTable from 'components/GTable/GTable'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeam.helpers'; -import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; +import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import UsersFilters from 'components/UsersFilters/UsersFilters'; @@ -41,10 +41,7 @@ interface UsersState { roles?: UserRole[]; }; - notFound?: boolean; - wrongTeamError?: boolean; - teamToSwitch?: { name: string; id: string }; - wrongTeamNoPermissions?: boolean; + wrongTeamData: WrongTeamData; } @observer @@ -58,8 +55,7 @@ class Users extends React.Component { roles: [UserRole.ADMIN, UserRole.EDITOR, UserRole.VIEWER], }, - wrongTeamError: false, - wrongTeamNoPermissions: false, + wrongTeamData: initWrongTeamDataState(), }; initialUsersLoaded = false; @@ -100,7 +96,7 @@ class Users extends React.Component { } parseParams = async () => { - this.setState({ wrongTeamError: false }); // reset wrong team error to false on query parse + this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse const { store, @@ -109,7 +105,7 @@ class Users extends React.Component { if (id) { await (id === 'me' ? store.userStore.loadCurrentUser() : store.userStore.loadUser(String(id), true)).catch( - (error) => this.setState({ ...getWrongTeamResponseInfo(error) }) + (error) => this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } }) ); const userPkToEdit = String(id === 'me' ? store.userStore.currentUserPk : id); @@ -121,7 +117,7 @@ class Users extends React.Component { }; render() { - const { usersFilters, userPkToEdit, page, wrongTeamError, teamToSwitch, wrongTeamNoPermissions } = this.state; + const { usersFilters, userPkToEdit, page, wrongTeamData } = this.state; const { store } = this.props; const { userStore } = store; @@ -173,76 +169,77 @@ class Users extends React.Component { const { count, results } = userStore.getSearchResult(); - if (wrongTeamError) { - return ( - - ); - } - return ( -
-
-
-
-
- Users - - To manage permissions or add users, please visit Grafana user management - -
-
- - - -
- {store.isUserActionAllowed(UserAction.ViewOtherUsers) ? ( - <> -
- - -
+ + {() => ( + <> +
+
+
+
+
+ Users + + To manage permissions or add users, please visit{' '} + Grafana user management + +
+
+ + + +
+ {store.isUserActionAllowed(UserAction.ViewOtherUsers) ? ( + <> +
+ + +
- - - ) : ( - - You don't have enough permissions to view other users because you are not Admin.{' '} - Click here to open your profile - - } - severity="info" - /> - )} -
- {userPkToEdit && } -
+ + + ) : ( + + You don't have enough permissions to view other users because you are not Admin.{' '} + Click here to open your profile + + } + severity="info" + /> + )} +
+ {userPkToEdit && } +
+ + )} + ); } From 6ee247a231dc3b31a67b4a33e3ea4db857b7c924 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 20 Sep 2022 17:25:46 +0300 Subject: [PATCH 36/89] better naming --- .../WrongTeamDisplayWrapper.helpers.tsx | 2 +- .../WrongTeamDisplayWrapper.module.css | 0 .../WrongTeamDisplayWrapper.tsx | 2 +- .../src/pages/escalation-chains/EscalationChains.tsx | 4 ++-- grafana-plugin/src/pages/incident/Incident.tsx | 4 ++-- grafana-plugin/src/pages/integrations/Integrations.tsx | 4 ++-- .../src/pages/outgoing_webhooks/OutgoingWebhooks.tsx | 4 ++-- grafana-plugin/src/pages/schedules/Schedules.tsx | 4 ++-- grafana-plugin/src/pages/users/Users.tsx | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) rename grafana-plugin/src/components/{NotFoundInTeam => WrongTeamDisplayWrapper}/WrongTeamDisplayWrapper.helpers.tsx (85%) rename grafana-plugin/src/components/{NotFoundInTeam => WrongTeamDisplayWrapper}/WrongTeamDisplayWrapper.module.css (100%) rename grafana-plugin/src/components/{NotFoundInTeam => WrongTeamDisplayWrapper}/WrongTeamDisplayWrapper.tsx (97%) diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers.tsx b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx similarity index 85% rename from grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers.tsx rename to grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx index 71338188..5b72008a 100644 --- a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers.tsx +++ b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx @@ -1,4 +1,4 @@ -import { WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; +import { WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; export function getWrongTeamResponseInfo({ response }): Partial { if (response) { diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.module.css b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.module.css similarity index 100% rename from grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.module.css rename to grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.module.css diff --git a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.tsx b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx similarity index 97% rename from grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.tsx rename to grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx index b940b69b..6502d360 100644 --- a/grafana-plugin/src/components/NotFoundInTeam/WrongTeamDisplayWrapper.tsx +++ b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx @@ -9,7 +9,7 @@ import { ChangeTeamIcon } from 'icons'; import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; import { useStore } from 'state/useStore'; -import styles from './WrongTeamWrapperDisplay.module.css'; +import styles from './WrongTeamDisplayWrapper.module.css'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index fac11f4b..e79410b4 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -11,13 +11,13 @@ import Collapse from 'components/Collapse/Collapse'; import EscalationsFilters from 'components/EscalationsFilters/EscalationsFilters'; import Block from 'components/GBlock/Block'; import GList from 'components/GList/GList'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; import WithConfirm from 'components/WithConfirm/WithConfirm'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import EscalationChainCard from 'containers/EscalationChainCard/EscalationChainCard'; import EscalationChainForm from 'containers/EscalationChainForm/EscalationChainForm'; import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps'; diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 006f2580..0e538a29 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -26,11 +26,11 @@ import reactStringReplace from 'react-string-replace'; import Collapse from 'components/Collapse/Collapse'; import Block from 'components/GBlock/Block'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import AttachIncidentForm from 'containers/AttachIncidentForm/AttachIncidentForm'; import IntegrationSettings from 'containers/IntegrationSettings/IntegrationSettings'; import { IntegrationSettingsTab } from 'containers/IntegrationSettings/IntegrationSettings.types'; diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 8251143f..3e7a1b0e 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -9,11 +9,11 @@ import { observer } from 'mobx-react'; import GList from 'components/GList/GList'; import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/IntegrationsFilters'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import AlertReceiveChannelCard from 'containers/AlertReceiveChannelCard/AlertReceiveChannelCard'; import AlertRules from 'containers/AlertRules/AlertRules'; import CreateAlertReceiveChannelContainer from 'containers/CreateAlertReceiveChannelContainer/CreateAlertReceiveChannelContainer'; diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index ed1221e1..2f162d8a 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -7,11 +7,11 @@ import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import GTable from 'components/GTable/GTable'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import WithConfirm from 'components/WithConfirm/WithConfirm'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookForm'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { ActionDTO } from 'models/action'; diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 5402a3e4..084108db 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -22,14 +22,14 @@ import moment, { Moment } from 'moment-timezone'; import instructionsImage from 'assets/img/events_instructions.png'; import Avatar from 'components/Avatar/Avatar'; import GTable from 'components/GTable/GTable'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import SchedulesFilters from 'components/SchedulesFilters/SchedulesFilters'; import { SchedulesFiltersType } from 'components/SchedulesFilters/SchedulesFilters.types'; import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import GSelect from 'containers/GSelect/GSelect'; import ScheduleForm from 'containers/ScheduleForm/ScheduleForm'; import ScheduleICalSettings from 'containers/ScheduleIcalLink/ScheduleIcalLink'; diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index d971d4b8..493428a9 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -9,11 +9,11 @@ import { observer } from 'mobx-react'; import Avatar from 'components/Avatar/Avatar'; import GTable from 'components/GTable/GTable'; -import { getWrongTeamResponseInfo } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper.helpers'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/NotFoundInTeam/WrongTeamDisplayWrapper'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import UsersFilters from 'components/UsersFilters/UsersFilters'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import UserSettings from 'containers/UserSettings/UserSettings'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { getRole } from 'models/user/user.helpers'; From 05199570c2e6e7ccf6bead841b2c78fa9d78c803 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 20 Sep 2022 17:27:06 +0300 Subject: [PATCH 37/89] typing for children prop --- .../WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx index 6502d360..15e135e0 100644 --- a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx +++ b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx @@ -33,7 +33,7 @@ export default function WrongTeamDisplayWrapper({ wrongTeamData: WrongTeamData; objectName: string; pageName: string; - children: any; + children: () => JSX.Element; }) { if (!wrongTeamData.isError) return children(); From 49ea882fcf3e8c1cf010ae1c78a53f8dc4be9f7c Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 21 Sep 2022 10:55:58 +0300 Subject: [PATCH 38/89] extends base state --- .../WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx | 4 ++++ .../src/pages/escalation-chains/EscalationChains.tsx | 5 ++--- grafana-plugin/src/pages/incident/Incident.tsx | 5 ++--- grafana-plugin/src/pages/integrations/Integrations.tsx | 5 ++--- .../src/pages/outgoing_webhooks/OutgoingWebhooks.tsx | 5 ++--- grafana-plugin/src/pages/schedules/Schedules.tsx | 5 ++--- grafana-plugin/src/pages/users/Users.tsx | 6 ++---- 7 files changed, 16 insertions(+), 19 deletions(-) diff --git a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx index 15e135e0..d427f65a 100644 --- a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx +++ b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx @@ -13,6 +13,10 @@ import styles from './WrongTeamDisplayWrapper.module.css'; const cx = cn.bind(styles); +export interface PageBaseState { + wrongTeamData: WrongTeamData; +} + export interface WrongTeamData { notFound?: boolean; isError?: boolean; diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index e79410b4..bf54b75b 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -16,7 +16,7 @@ import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; import WithConfirm from 'components/WithConfirm/WithConfirm'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import EscalationChainCard from 'containers/EscalationChainCard/EscalationChainCard'; import EscalationChainForm from 'containers/EscalationChainForm/EscalationChainForm'; @@ -34,12 +34,11 @@ const cx = cn.bind(styles); interface EscalationChainsPageProps extends WithStoreProps, AppRootProps {} -interface EscalationChainsPageState { +interface EscalationChainsPageState extends PageBaseState { escalationChainsFilters: { searchTerm: string }; showCreateEscalationChainModal: boolean; escalationChainIdToCopy: EscalationChain['id']; selectedEscalationChain: EscalationChain['id']; - wrongTeamData: WrongTeamData; } export interface Filters { diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 0e538a29..2fd4431f 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -29,7 +29,7 @@ import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; import PluginLink from 'components/PluginLink/PluginLink'; import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import AttachIncidentForm from 'containers/AttachIncidentForm/AttachIncidentForm'; import IntegrationSettings from 'containers/IntegrationSettings/IntegrationSettings'; @@ -59,12 +59,11 @@ const cx = cn.bind(styles); interface IncidentPageProps extends WithStoreProps, AppRootProps {} -interface IncidentPageState { +interface IncidentPageState extends PageBaseState { showIntegrationSettings?: boolean; showAttachIncidentForm?: boolean; timelineFilter: string; resolutionNoteText: string; - wrongTeamData: WrongTeamData; } @observer diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 3e7a1b0e..93073e66 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -12,7 +12,7 @@ import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/Int import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import AlertReceiveChannelCard from 'containers/AlertReceiveChannelCard/AlertReceiveChannelCard'; import AlertRules from 'containers/AlertRules/AlertRules'; @@ -31,12 +31,11 @@ import styles from './Integrations.module.css'; const cx = cn.bind(styles); -interface IntegrationsState { +interface IntegrationsState extends PageBaseState { integrationsFilters: Filters; showCreateIntegrationModal: boolean; alertReceiveChannelToShowSettings?: AlertReceiveChannel['id']; integrationSettingsTab?: IntegrationSettingsTab; - wrongTeamData: WrongTeamData; } interface IntegrationsProps extends WithStoreProps, AppRootProps {} diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 2f162d8a..7fb3f643 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -10,7 +10,7 @@ import GTable from 'components/GTable/GTable'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import WithConfirm from 'components/WithConfirm/WithConfirm'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookForm'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; @@ -26,9 +26,8 @@ const cx = cn.bind(styles); interface OutgoingWebhooksProps extends WithStoreProps, AppRootProps {} -interface OutgoingWebhooksState { +interface OutgoingWebhooksState extends PageBaseState { outgoingWebhookIdToEdit?: OutgoingWebhook['id'] | 'new'; - wrongTeamData: WrongTeamData; } @observer diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 084108db..de35df6d 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -28,7 +28,7 @@ import { SchedulesFiltersType } from 'components/SchedulesFilters/SchedulesFilte import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import GSelect from 'containers/GSelect/GSelect'; import ScheduleForm from 'containers/ScheduleForm/ScheduleForm'; @@ -49,13 +49,12 @@ import styles from './Schedules.module.css'; const cx = cn.bind(styles); interface SchedulesPageProps extends WithStoreProps, AppRootProps {} -interface SchedulesPageState { +interface SchedulesPageState extends PageBaseState { scheduleIdToEdit?: Schedule['id']; scheduleIdToDelete?: Schedule['id']; scheduleIdToExport?: Schedule['id']; filters: SchedulesFiltersType; expandedSchedulesKeys: Array; - wrongTeamData: WrongTeamData; } @observer diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index 493428a9..7878e753 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -12,7 +12,7 @@ import GTable from 'components/GTable/GTable'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import UsersFilters from 'components/UsersFilters/UsersFilters'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import UserSettings from 'containers/UserSettings/UserSettings'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; @@ -32,7 +32,7 @@ interface UsersProps extends WithStoreProps, AppRootProps {} const ITEMS_PER_PAGE = 100; -interface UsersState { +interface UsersState extends PageBaseState { page: number; isWrongTeam: boolean; userPkToEdit?: UserType['pk'] | 'new'; @@ -40,8 +40,6 @@ interface UsersState { searchTerm: string; roles?: UserRole[]; }; - - wrongTeamData: WrongTeamData; } @observer From 761a79e1166229f4b77f866aef57d4e906e36bbb Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 21 Sep 2022 13:43:13 +0300 Subject: [PATCH 39/89] not found notification --- .../WrongTeamDisplayWrapper.helpers.tsx | 4 +++ .../WrongTeamDisplayWrapper.tsx | 15 ++++++--- .../escalation-chains/EscalationChains.tsx | 22 +++++++------ .../src/pages/incident/Incident.tsx | 31 ++++++------------- .../src/pages/integrations/Integrations.tsx | 20 +++++++----- .../outgoing_webhooks/OutgoingWebhooks.tsx | 16 +++++++--- .../src/pages/schedules/Schedules.tsx | 18 +++++++---- grafana-plugin/src/pages/users/Users.tsx | 16 +++++++--- 8 files changed, 84 insertions(+), 58 deletions(-) diff --git a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx index 5b72008a..5c7b5dac 100644 --- a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx +++ b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx @@ -1,5 +1,9 @@ import { WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +export function initWrongTeamDataState(): Partial { + return { isError: false, wrongTeamNoPermissions: false }; +} + export function getWrongTeamResponseInfo({ response }): Partial { if (response) { if (response.status === 404) { diff --git a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx index d427f65a..6a41c025 100644 --- a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx +++ b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Button, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; @@ -10,6 +10,7 @@ import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; import { useStore } from 'state/useStore'; import styles from './WrongTeamDisplayWrapper.module.css'; +import { openWarningNotification } from 'utils'; const cx = cn.bind(styles); @@ -24,21 +25,25 @@ export interface WrongTeamData { switchToTeam?: { name: string; id: string }; } -export function initWrongTeamDataState(): Partial { - return { isError: false, wrongTeamNoPermissions: false }; -} - export default function WrongTeamDisplayWrapper({ wrongTeamData, objectName, pageName, + itemNotFoundMessage, children, }: { wrongTeamData: WrongTeamData; objectName: string; pageName: string; + itemNotFoundMessage: string; children: () => JSX.Element; }) { + useEffect(() => { + if (!wrongTeamData.isError && wrongTeamData.notFound) { + openWarningNotification(itemNotFoundMessage); + } + }, [wrongTeamData.notFound]); + if (!wrongTeamData.isError) return children(); const store = useStore(); diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index bf54b75b..7cd64739 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -16,8 +16,11 @@ import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; import WithConfirm from 'components/WithConfirm/WithConfirm'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; -import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { + getWrongTeamResponseInfo, + initWrongTeamDataState, +} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import EscalationChainCard from 'containers/EscalationChainCard/EscalationChainCard'; import EscalationChainForm from 'containers/EscalationChainForm/EscalationChainForm'; import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps'; @@ -60,7 +63,7 @@ class EscalationChainsPage extends React.Component { - this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse + this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset on query parse const { store, query } = this.props; const { escalationChainStore } = store; @@ -83,10 +86,6 @@ class EscalationChainsPage extends React.Component + {() => ( <>
diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 2fd4431f..dde6d254 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -29,8 +29,8 @@ import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; import PluginLink from 'components/PluginLink/PluginLink'; import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; -import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { getWrongTeamResponseInfo, initWrongTeamDataState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import AttachIncidentForm from 'containers/AttachIncidentForm/AttachIncidentForm'; import IntegrationSettings from 'containers/IntegrationSettings/IntegrationSettings'; import { IntegrationSettingsTab } from 'containers/IntegrationSettings/IntegrationSettings.types'; @@ -89,7 +89,7 @@ class IncidentPage extends React.Component } update = () => { - this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse // reset wrong team error to false + this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false const { store, @@ -114,24 +114,6 @@ class IncidentPage extends React.Component const incident = alerts.get(id); - // if (notFound) { - // return ( - //
- //
- // - // 404 - // Incident not found - // - // - // - // - //
- //
- // ); - // } - if (!incident && !wrongTeamData.isError) { return (
@@ -141,7 +123,12 @@ class IncidentPage extends React.Component } return ( - + {() => ( <>
diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 93073e66..85b8c2b5 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -12,8 +12,11 @@ import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/Int import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; -import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { + getWrongTeamResponseInfo, + initWrongTeamDataState, +} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import AlertReceiveChannelCard from 'containers/AlertReceiveChannelCard/AlertReceiveChannelCard'; import AlertRules from 'containers/AlertRules/AlertRules'; import CreateAlertReceiveChannelContainer from 'containers/CreateAlertReceiveChannelContainer/CreateAlertReceiveChannelContainer'; @@ -81,10 +84,6 @@ class Integrations extends React.Component if (alertReceiveChannel.id) { selectedAlertReceiveChannel = alertReceiveChannel.id; - } else { - openWarningNotification( - `Integration with id=${query?.id} is not found. Please select integration from the list.` - ); } if (query.tab) { @@ -119,7 +118,7 @@ class Integrations extends React.Component } render() { - const { store } = this.props; + const { store, query } = this.props; const { integrationsFilters, alertReceiveChannelToShowSettings, @@ -132,7 +131,12 @@ class Integrations extends React.Component const searchResult = alertReceiveChannelStore.getSearchResult(); return ( - + {() => ( <>
diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 7fb3f643..849fd39a 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -10,8 +10,11 @@ import GTable from 'components/GTable/GTable'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import WithConfirm from 'components/WithConfirm/WithConfirm'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; -import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { + getWrongTeamResponseInfo, + initWrongTeamDataState, +} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookForm'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { ActionDTO } from 'models/action'; @@ -73,7 +76,7 @@ class OutgoingWebhooks extends React.Component + {() => ( <>
diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index de35df6d..86d59213 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -28,14 +28,15 @@ import { SchedulesFiltersType } from 'components/SchedulesFilters/SchedulesFilte import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; -import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; -import GSelect from 'containers/GSelect/GSelect'; +import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { + getWrongTeamResponseInfo, + initWrongTeamDataState, +} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import ScheduleForm from 'containers/ScheduleForm/ScheduleForm'; import ScheduleICalSettings from 'containers/ScheduleIcalLink/ScheduleIcalLink'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { Schedule, ScheduleEvent } from 'models/schedule/schedule.types'; -import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config'; import { getSlackChannelName } from 'models/slack_channel/slack_channel.helpers'; import { WithStoreProps } from 'state/types'; import { UserAction } from 'state/userAction'; @@ -111,7 +112,7 @@ class SchedulesPage extends React.Component + {() => ( <>
diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index 7878e753..aeba9f35 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -12,8 +12,11 @@ import GTable from 'components/GTable/GTable'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import UsersFilters from 'components/UsersFilters/UsersFilters'; -import WrongTeamDisplayWrapper, { initWrongTeamDataState, PageBaseState, WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; -import { getWrongTeamResponseInfo } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; +import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import { + getWrongTeamResponseInfo, + initWrongTeamDataState, +} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; import UserSettings from 'containers/UserSettings/UserSettings'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { getRole } from 'models/user/user.helpers'; @@ -116,7 +119,7 @@ class Users extends React.Component { render() { const { usersFilters, userPkToEdit, page, wrongTeamData } = this.state; - const { store } = this.props; + const { store, query } = this.props; const { userStore } = store; const columns = [ @@ -168,7 +171,12 @@ class Users extends React.Component { const { count, results } = userStore.getSearchResult(); return ( - + {() => ( <>
From 5441a4da95c60d2cd36f6ef1f7e10a4ee13095bf Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 21 Sep 2022 14:43:50 +0300 Subject: [PATCH 40/89] improvements :) --- .../PageErrorHandlingWrapper.helpers.tsx | 22 +++ .../PageErrorHandlingWrapper.module.css} | 0 .../PageErrorHandlingWrapper.tsx} | 28 ++-- .../WrongTeamDisplayWrapper.helpers.tsx | 22 --- .../escalation-chains/EscalationChains.tsx | 20 +-- .../src/pages/incident/Incident.tsx | 130 ++++++++++-------- .../src/pages/integrations/Integrations.tsx | 20 +-- .../outgoing_webhooks/OutgoingWebhooks.tsx | 30 ++-- .../src/pages/schedules/Schedules.tsx | 20 +-- grafana-plugin/src/pages/users/Users.tsx | 20 +-- 10 files changed, 167 insertions(+), 145 deletions(-) create mode 100644 grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers.tsx rename grafana-plugin/src/components/{WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.module.css => PageErrorHandlingWrapper/PageErrorHandlingWrapper.module.css} (100%) rename grafana-plugin/src/components/{WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx => PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx} (79%) delete mode 100644 grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx diff --git a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers.tsx b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers.tsx new file mode 100644 index 00000000..840e0945 --- /dev/null +++ b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers.tsx @@ -0,0 +1,22 @@ +import { PageErrorData } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; + +export function initErrorDataState(): Partial { + return { isWrongTeamError: false, wrongTeamNoPermissions: false }; +} + +export function getWrongTeamResponseInfo({ response }): Partial { + if (response) { + if (response.status === 404) { + return { isNotFoundError: true }; + } else if (response.status === 403 && response.data.error_code === 'wrong_team') { + let res = response.data; + if (res.owner_team) { + return { isWrongTeamError: true, switchToTeam: { name: res.owner_team.name, id: res.owner_team.id } }; + } else { + return { isWrongTeamError: true, wrongTeamNoPermissions: true }; + } + } + } + + return { isNotFoundError: true }; +} diff --git a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.module.css b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.module.css similarity index 100% rename from grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.module.css rename to grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.module.css diff --git a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx similarity index 79% rename from grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx rename to grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx index 6a41c025..33f333e6 100644 --- a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.tsx +++ b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx @@ -9,49 +9,51 @@ import { ChangeTeamIcon } from 'icons'; import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; import { useStore } from 'state/useStore'; -import styles from './WrongTeamDisplayWrapper.module.css'; +import styles from './PageErrorHandlingWrapper.module.css'; import { openWarningNotification } from 'utils'; +import { PropTypes } from 'mobx-react'; const cx = cn.bind(styles); export interface PageBaseState { - wrongTeamData: WrongTeamData; + errorData: PageErrorData; } -export interface WrongTeamData { - notFound?: boolean; - isError?: boolean; +export interface PageErrorData { + isNotFoundError?: boolean; + isWrongTeamError?: boolean; wrongTeamNoPermissions?: boolean; switchToTeam?: { name: string; id: string }; } -export default function WrongTeamDisplayWrapper({ - wrongTeamData, +export default function PageErrorHandlingWrapper({ + errorData, objectName, pageName, itemNotFoundMessage, children, }: { - wrongTeamData: WrongTeamData; + errorData: PageErrorData; objectName: string; pageName: string; - itemNotFoundMessage: string; + itemNotFoundMessage?: string; children: () => JSX.Element; }) { useEffect(() => { - if (!wrongTeamData.isError && wrongTeamData.notFound) { + const { isWrongTeamError, isNotFoundError } = errorData; + if (!isWrongTeamError && isNotFoundError && itemNotFoundMessage) { openWarningNotification(itemNotFoundMessage); } - }, [wrongTeamData.notFound]); + }, [errorData.isNotFoundError]); - if (!wrongTeamData.isError) return children(); + if (!errorData.isWrongTeamError) return children(); const store = useStore(); const currentTeamId = store.userStore.currentUser?.current_team; const currentTeam = store.grafanaTeamStore.items[currentTeamId]?.name; - const { switchToTeam, wrongTeamNoPermissions } = wrongTeamData; + const { switchToTeam, wrongTeamNoPermissions } = errorData; const onTeamChange = async (teamId: GrafanaTeam['id']) => { await store.userStore.updateCurrentUser({ current_team: teamId }); diff --git a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx b/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx deleted file mode 100644 index 5c7b5dac..00000000 --- a/grafana-plugin/src/components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { WrongTeamData } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; - -export function initWrongTeamDataState(): Partial { - return { isError: false, wrongTeamNoPermissions: false }; -} - -export function getWrongTeamResponseInfo({ response }): Partial { - if (response) { - if (response.status === 404) { - return { notFound: true }; - } else if (response.status === 403 && response.data.error_code === 'wrong_team') { - let res = response.data; - if (res.owner_team) { - return { isError: true, switchToTeam: { name: res.owner_team.name, id: res.owner_team.id } }; - } else { - return { isError: true, wrongTeamNoPermissions: true }; - } - } - } - - return { notFound: true }; -} diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 7cd64739..9bddf331 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -16,11 +16,11 @@ import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; import WithConfirm from 'components/WithConfirm/WithConfirm'; -import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, - initWrongTeamDataState, -} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; + initErrorDataState, +} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import EscalationChainCard from 'containers/EscalationChainCard/EscalationChainCard'; import EscalationChainForm from 'containers/EscalationChainForm/EscalationChainForm'; import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps'; @@ -55,7 +55,7 @@ class EscalationChainsPage extends React.Component { - this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset on query parse + this.setState({ errorData: initErrorDataState() }); // reset on query parse const { store, query } = this.props; const { escalationChainStore } = store; @@ -77,7 +77,7 @@ class EscalationChainsPage extends React.Component this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); + .catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } })); if (!escalationChain) { return; @@ -128,15 +128,15 @@ class EscalationChainsPage extends React.Component )} - + ); } diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index dde6d254..f92a95be 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -29,8 +29,11 @@ import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; import PluginLink from 'components/PluginLink/PluginLink'; import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; -import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; -import { getWrongTeamResponseInfo, initWrongTeamDataState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; +import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; +import { + getWrongTeamResponseInfo, + initErrorDataState, +} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import AttachIncidentForm from 'containers/AttachIncidentForm/AttachIncidentForm'; import IntegrationSettings from 'containers/IntegrationSettings/IntegrationSettings'; import { IntegrationSettingsTab } from 'containers/IntegrationSettings/IntegrationSettings.types'; @@ -71,7 +74,7 @@ class IncidentPage extends React.Component state: IncidentPageState = { timelineFilter: 'all', resolutionNoteText: '', - wrongTeamData: initWrongTeamDataState(), + errorData: initErrorDataState(), }; componentDidMount() { @@ -89,7 +92,7 @@ class IncidentPage extends React.Component } update = () => { - this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false + this.setState({ errorData: initErrorDataState() }); // reset wrong team error to false const { store, @@ -98,7 +101,7 @@ class IncidentPage extends React.Component store.alertGroupStore .getAlert(id) - .catch((error) => this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); + .catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } })); }; render() { @@ -107,14 +110,14 @@ class IncidentPage extends React.Component query: { id, cursor, start, perpage }, } = this.props; - const { wrongTeamData, showIntegrationSettings, showAttachIncidentForm } = this.state; - + const { errorData, showIntegrationSettings, showAttachIncidentForm } = this.state; + const { isNotFoundError, isWrongTeamError } = errorData; const { alertReceiveChannelStore } = store; const { alerts } = store.alertGroupStore; const incident = alerts.get(id); - if (!incident && !wrongTeamData.isError) { + if (!incident && !isNotFoundError && !isWrongTeamError) { return (
@@ -123,60 +126,75 @@ class IncidentPage extends React.Component } return ( - - {() => ( - <> + {() => + errorData.isNotFoundError ? (
- {this.renderHeader()} -
-
- - - -
-
{this.renderTimeline()}
+
+ + 404 + Incident not found + + + +
- {showIntegrationSettings && ( - { - alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id); - }} - onUpdateTemplates={() => { - store.alertGroupStore.getAlert(id); - }} - startTab={IntegrationSettingsTab.Templates} - id={incident.alert_receive_channel.id} - onHide={() => - this.setState({ - showIntegrationSettings: undefined, - }) - } - /> - )} - {showAttachIncidentForm && ( - { - this.setState({ - showAttachIncidentForm: false, - }); - }} - onUpdate={this.update} - /> - )} - - )} - + ) : ( + <> +
+ {this.renderHeader()} +
+
+ + + +
+
{this.renderTimeline()}
+
+
+ {showIntegrationSettings && ( + { + alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id); + }} + onUpdateTemplates={() => { + store.alertGroupStore.getAlert(id); + }} + startTab={IntegrationSettingsTab.Templates} + id={incident.alert_receive_channel.id} + onHide={() => + this.setState({ + showIntegrationSettings: undefined, + }) + } + /> + )} + {showAttachIncidentForm && ( + { + this.setState({ + showAttachIncidentForm: false, + }); + }} + onUpdate={this.update} + /> + )} + + ) + } + ); } diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 85b8c2b5..c7f81d16 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -12,11 +12,11 @@ import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/Int import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; -import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, - initWrongTeamDataState, -} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; + initErrorDataState, +} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import AlertReceiveChannelCard from 'containers/AlertReceiveChannelCard/AlertReceiveChannelCard'; import AlertRules from 'containers/AlertRules/AlertRules'; import CreateAlertReceiveChannelContainer from 'containers/CreateAlertReceiveChannelContainer/CreateAlertReceiveChannelContainer'; @@ -48,7 +48,7 @@ class Integrations extends React.Component state: IntegrationsState = { integrationsFilters: { searchTerm: '' }, showCreateIntegrationModal: false, - wrongTeamData: initWrongTeamDataState(), + errorData: initErrorDataState(), }; alertReceiveChanneltoPoll: { [key: string]: number } = {}; @@ -65,7 +65,7 @@ class Integrations extends React.Component }; parseQueryParams = async () => { - this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse // reset wrong team error to false + this.setState({ errorData: initErrorDataState() }); // reset wrong team error to false on query parse // reset wrong team error to false const { store, query } = this.props; const { alertReceiveChannelStore } = store; @@ -76,7 +76,7 @@ class Integrations extends React.Component if (query.id) { let alertReceiveChannel = await alertReceiveChannelStore .loadItem(query.id, true) - .catch((error) => this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); + .catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } })); if (!alertReceiveChannel) { return; @@ -124,15 +124,15 @@ class Integrations extends React.Component alertReceiveChannelToShowSettings, integrationSettingsTab, showCreateIntegrationModal, - wrongTeamData, + errorData, } = this.state; const { alertReceiveChannelStore } = store; const searchResult = alertReceiveChannelStore.getSearchResult(); return ( - )} )} - + ); } diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 849fd39a..e48f10aa 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -10,11 +10,11 @@ import GTable from 'components/GTable/GTable'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import WithConfirm from 'components/WithConfirm/WithConfirm'; -import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, - initWrongTeamDataState, -} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; + initErrorDataState, +} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookForm'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { ActionDTO } from 'models/action'; @@ -36,7 +36,7 @@ interface OutgoingWebhooksState extends PageBaseState { @observer class OutgoingWebhooks extends React.Component { state: OutgoingWebhooksState = { - wrongTeamData: initWrongTeamDataState(), + errorData: initErrorDataState(), }; async componentDidMount() { @@ -51,7 +51,7 @@ class OutgoingWebhooks extends React.Component { this.setState((prevState) => ({ - wrongTeamData: initWrongTeamDataState(), + errorData: initErrorDataState(), outgoingWebhookIdToEdit: undefined, })); // reset state on query parse @@ -60,12 +60,14 @@ class OutgoingWebhooks extends React.Component this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); - if (id) { - this.setState({ outgoingWebhookIdToEdit: id }); + const outgoingWebhook = await store.outgoingWebhookStore + .loadItem(id, true) + .catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } })); + + if (outgoingWebhook) { + this.setState({ outgoingWebhookIdToEdit: id }); + } } }; @@ -77,7 +79,7 @@ class OutgoingWebhooks extends React.Component )} - + ); } diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 86d59213..97cb3112 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -28,11 +28,11 @@ import { SchedulesFiltersType } from 'components/SchedulesFilters/SchedulesFilte import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; -import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, - initWrongTeamDataState, -} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; + initErrorDataState, +} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import ScheduleForm from 'containers/ScheduleForm/ScheduleForm'; import ScheduleICalSettings from 'containers/ScheduleIcalLink/ScheduleIcalLink'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; @@ -65,7 +65,7 @@ class SchedulesPage extends React.Component { - this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse + this.setState({ errorData: initErrorDataState() }); // reset wrong team error to false on query parse const { store, @@ -89,7 +89,7 @@ class SchedulesPage extends React.Component this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } })); + .catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } })); if (!schedule) { return; } @@ -114,7 +114,7 @@ class SchedulesPage extends React.Component )} - + ); } diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index aeba9f35..ffdc92c4 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -12,11 +12,11 @@ import GTable from 'components/GTable/GTable'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import UsersFilters from 'components/UsersFilters/UsersFilters'; -import WrongTeamDisplayWrapper, { PageBaseState } from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper'; +import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, - initWrongTeamDataState, -} from 'components/WrongTeamDisplayWrapper/WrongTeamDisplayWrapper.helpers'; + initErrorDataState, +} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import UserSettings from 'containers/UserSettings/UserSettings'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { getRole } from 'models/user/user.helpers'; @@ -56,7 +56,7 @@ class Users extends React.Component { roles: [UserRole.ADMIN, UserRole.EDITOR, UserRole.VIEWER], }, - wrongTeamData: initWrongTeamDataState(), + errorData: initErrorDataState(), }; initialUsersLoaded = false; @@ -97,7 +97,7 @@ class Users extends React.Component { } parseParams = async () => { - this.setState({ wrongTeamData: initWrongTeamDataState() }); // reset wrong team error to false on query parse + this.setState({ errorData: initErrorDataState() }); // reset wrong team error to false on query parse const { store, @@ -106,7 +106,7 @@ class Users extends React.Component { if (id) { await (id === 'me' ? store.userStore.loadCurrentUser() : store.userStore.loadUser(String(id), true)).catch( - (error) => this.setState({ wrongTeamData: { ...getWrongTeamResponseInfo(error) } }) + (error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } }) ); const userPkToEdit = String(id === 'me' ? store.userStore.currentUserPk : id); @@ -118,7 +118,7 @@ class Users extends React.Component { }; render() { - const { usersFilters, userPkToEdit, page, wrongTeamData } = this.state; + const { usersFilters, userPkToEdit, page, errorData } = this.state; const { store, query } = this.props; const { userStore } = store; @@ -171,8 +171,8 @@ class Users extends React.Component { const { count, results } = userStore.getSearchResult(); return ( - {
)} -
+ ); } From 2988c10c92371a982b6e284ffa6bacd8cd72c4e0 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Thu, 22 Sep 2022 14:35:37 -0300 Subject: [PATCH 41/89] Fix outgoing webhook to resolve IP from parsed hostname --- engine/apps/alerts/tests/test_utils.py | 8 ++++++++ engine/apps/alerts/utils.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/engine/apps/alerts/tests/test_utils.py b/engine/apps/alerts/tests/test_utils.py index ff19018a..7934f64a 100644 --- a/engine/apps/alerts/tests/test_utils.py +++ b/engine/apps/alerts/tests/test_utils.py @@ -12,3 +12,11 @@ def test_request_outgoing_webhook_cannot_resolve_name(): success, err = request_outgoing_webhook("http://something.something/webhook", "GET") assert success is False assert err == "Cannot resolve name in url" + + +@pytest.mark.django_db +def test_request_outgoing_webhook_resolve_name_without_port(): + with patch("apps.alerts.utils.socket.gethostbyname") as mock_gethostbyname: + mock_gethostbyname.return_value = "127.0.0.1" + request_outgoing_webhook("http://something.something:9000/webhook", "GET") + assert mock_gethostbyname.call_args_list[0].args[0] == "something.something" diff --git a/engine/apps/alerts/utils.py b/engine/apps/alerts/utils.py index 58ba22ea..86cbc786 100644 --- a/engine/apps/alerts/utils.py +++ b/engine/apps/alerts/utils.py @@ -57,7 +57,7 @@ def request_outgoing_webhook(webhook_url, http_request_type, post_kwargs={}) -> if not live_settings.DANGEROUS_WEBHOOKS_ENABLED: # Get the ip address of the webhook url and check if it belongs to the private network try: - webhook_url_ip_address = socket.gethostbyname(parsed_url.netloc) + webhook_url_ip_address = socket.gethostbyname(parsed_url.hostname) except socket.gaierror: return False, "Cannot resolve name in url" if not live_settings.DANGEROUS_WEBHOOKS_ENABLED: From accee4ebbe6416172c78a412c90ba7d095c7d36d Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Fri, 23 Sep 2022 03:45:28 -0600 Subject: [PATCH 42/89] Use create_engine_url to add prefix to previous/next links (#553) * Use create_engine_url to add prefix to previous/next links * Remove override of get_paginated_response since it is unchanged from parent * More concise override * Make both overrides behave the same * add test for public API alert groups pagination Co-authored-by: Vadim Stepanov --- .../apps/public_api/tests/test_incidents.py | 19 ++++++++++++++++ engine/common/api_helpers/paginators.py | 22 +++++++++++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/engine/apps/public_api/tests/test_incidents.py b/engine/apps/public_api/tests/test_incidents.py index 360a5e5e..73685c25 100644 --- a/engine/apps/public_api/tests/test_incidents.py +++ b/engine/apps/public_api/tests/test_incidents.py @@ -1,4 +1,5 @@ from unittest import mock +from unittest.mock import patch import pytest from django.urls import reverse @@ -186,6 +187,24 @@ def test_delete_incident_invalid_request(incident_public_api_setup): assert response.status_code == status.HTTP_400_BAD_REQUEST +@pytest.mark.django_db +def test_pagination(settings, incident_public_api_setup): + settings.BASE_URL = "https://test.com/test/prefixed/urls" + + token, incidents, _, _ = incident_public_api_setup + client = APIClient() + + url = reverse("api-public:alert_groups-list") + + with patch("common.api_helpers.paginators.PathPrefixedPagination.get_page_size", return_value=1): + response = client.get(url, HTTP_AUTHORIZATION=f"{token}") + + assert response.status_code == status.HTTP_200_OK + result = response.json() + + assert result["next"].startswith("https://test.com/test/prefixed/urls") + + # This is test from old django-based tests # TODO: uncomment with date checking in delete mode # def test_delete_incident_invalid_date(self): diff --git a/engine/common/api_helpers/paginators.py b/engine/common/api_helpers/paginators.py index 01ce2cc6..2a3ad974 100644 --- a/engine/common/api_helpers/paginators.py +++ b/engine/common/api_helpers/paginators.py @@ -1,19 +1,33 @@ from rest_framework.pagination import CursorPagination, PageNumberPagination +from common.api_helpers.utils import create_engine_url -class HundredPageSizePaginator(PageNumberPagination): + +class PathPrefixedPagination(PageNumberPagination): + def paginate_queryset(self, queryset, request, view=None): + request.build_absolute_uri = lambda: create_engine_url(request.get_full_path()) + return super().paginate_queryset(queryset, request, view) + + +class PathPrefixedCursorPagination(CursorPagination): + def paginate_queryset(self, queryset, request, view=None): + request.build_absolute_uri = lambda: create_engine_url(request.get_full_path()) + return super().paginate_queryset(queryset, request, view) + + +class HundredPageSizePaginator(PathPrefixedPagination): page_size = 100 -class FiftyPageSizePaginator(PageNumberPagination): +class FiftyPageSizePaginator(PathPrefixedPagination): page_size = 50 -class TwentyFivePageSizePaginator(PageNumberPagination): +class TwentyFivePageSizePaginator(PathPrefixedPagination): page_size = 25 -class TwentyFiveCursorPaginator(CursorPagination): +class TwentyFiveCursorPaginator(PathPrefixedCursorPagination): page_size = 25 max_page_size = 100 page_size_query_param = "perpage" From 5859e883c5fccb18751fdc11712c83da24a17a4a Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Fri, 23 Sep 2022 12:10:01 +0100 Subject: [PATCH 43/89] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c13f348c..3f160463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## v1.0.37 (2022-09-21) +- Improve API token creation form +- Fix alert group bulk action bugs - Add `permalinks` property to `AlertGroup` public API response schema +- Scheduling system bug fixes +- Public API bug fixes ## v1.0.36 (2022-09-12) From 2dbd64105a3d99f185c4769a44a16662d27c3dde Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Fri, 23 Sep 2022 12:10:19 +0100 Subject: [PATCH 44/89] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f160463..df1284de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## v1.0.37 (2022-09-21) +## v1.0.37 (2022-09-23) - Improve API token creation form - Fix alert group bulk action bugs From cbb0f7c024f842a401f5c6beaa1bf4a32375bd3f Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 26 Sep 2022 14:06:30 +0300 Subject: [PATCH 45/89] build fix --- .../src/pages/schedules/Schedules.tsx | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 8c5ee23e..5da63196 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -180,6 +180,7 @@ class SchedulesPage extends React.Component
+ {!schedules || schedules.length ? ( )} - rowKey="id" - columns={columns} - data={schedules} - expandable={{ - expandedRowRender: this.renderEvents, - expandRowByClick: true, - onExpand: this.onRowExpand, - expandedRowKeys: expandedSchedulesKeys, - onExpandedRowsChange: this.handleExpandedRowsChange, - }} - /> - ) : ( - - You haven’t added a schedule yet. - - - - - } - /> - )} -
- {scheduleIdToEdit && ( - { - this.setState({ scheduleIdToEdit: undefined }); - getLocationSrv().update({ partial: true, query: { id: undefined } }); - }} - /> +
+ + {scheduleIdToEdit && ( + { + this.setState({ scheduleIdToEdit: undefined }); + getLocationSrv().update({ partial: true, query: { id: undefined } }); + }} + /> + )} + + {scheduleIdToDelete && ( + { + this.setState({ scheduleIdToDelete: undefined }); + }} + /> + )} + + {scheduleIdToExport && ( + this.setState({ scheduleIdToExport: undefined })} + > + + + )} + )} ); From 2bbddb5357acfdee56807310b5e92f55e2c4635d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 26 Sep 2022 14:10:36 +0300 Subject: [PATCH 46/89] linter --- .../PageErrorHandlingWrapper.tsx | 6 +++--- .../src/pages/escalation-chains/EscalationChains.tsx | 10 +++++----- grafana-plugin/src/pages/incident/Incident.tsx | 6 +++--- grafana-plugin/src/pages/integrations/Integrations.tsx | 6 +++--- .../src/pages/outgoing_webhooks/OutgoingWebhooks.tsx | 6 +++--- grafana-plugin/src/pages/schedules/Schedules.tsx | 10 +++++----- grafana-plugin/src/pages/users/Users.tsx | 6 +++--- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx index 33f333e6..f53193bd 100644 --- a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx +++ b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx @@ -2,16 +2,16 @@ import React, { useEffect } from 'react'; import { Button, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; +import { PropTypes } from 'mobx-react'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import { ChangeTeamIcon } from 'icons'; import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; import { useStore } from 'state/useStore'; +import { openWarningNotification } from 'utils'; import styles from './PageErrorHandlingWrapper.module.css'; -import { openWarningNotification } from 'utils'; -import { PropTypes } from 'mobx-react'; const cx = cn.bind(styles); @@ -46,7 +46,7 @@ export default function PageErrorHandlingWrapper({ } }, [errorData.isNotFoundError]); - if (!errorData.isWrongTeamError) return children(); + if (!errorData.isWrongTeamError) {return children();} const store = useStore(); diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 9bddf331..9b4109ed 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -11,16 +11,16 @@ import Collapse from 'components/Collapse/Collapse'; import EscalationsFilters from 'components/EscalationsFilters/EscalationsFilters'; import Block from 'components/GBlock/Block'; import GList from 'components/GList/GList'; -import PluginLink from 'components/PluginLink/PluginLink'; -import Text from 'components/Text/Text'; -import Tutorial from 'components/Tutorial/Tutorial'; -import { TutorialStep } from 'components/Tutorial/Tutorial.types'; -import WithConfirm from 'components/WithConfirm/WithConfirm'; import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, initErrorDataState, } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; +import PluginLink from 'components/PluginLink/PluginLink'; +import Text from 'components/Text/Text'; +import Tutorial from 'components/Tutorial/Tutorial'; +import { TutorialStep } from 'components/Tutorial/Tutorial.types'; +import WithConfirm from 'components/WithConfirm/WithConfirm'; import EscalationChainCard from 'containers/EscalationChainCard/EscalationChainCard'; import EscalationChainForm from 'containers/EscalationChainForm/EscalationChainForm'; import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps'; diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index f92a95be..9c7ee915 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -26,14 +26,14 @@ import reactStringReplace from 'react-string-replace'; import Collapse from 'components/Collapse/Collapse'; import Block from 'components/GBlock/Block'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; -import PluginLink from 'components/PluginLink/PluginLink'; -import SourceCode from 'components/SourceCode/SourceCode'; -import Text from 'components/Text/Text'; import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, initErrorDataState, } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; +import PluginLink from 'components/PluginLink/PluginLink'; +import SourceCode from 'components/SourceCode/SourceCode'; +import Text from 'components/Text/Text'; import AttachIncidentForm from 'containers/AttachIncidentForm/AttachIncidentForm'; import IntegrationSettings from 'containers/IntegrationSettings/IntegrationSettings'; import { IntegrationSettingsTab } from 'containers/IntegrationSettings/IntegrationSettings.types'; diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index c7f81d16..799671fd 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -9,14 +9,14 @@ import { observer } from 'mobx-react'; import GList from 'components/GList/GList'; import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/IntegrationsFilters'; -import Text from 'components/Text/Text'; -import Tutorial from 'components/Tutorial/Tutorial'; -import { TutorialStep } from 'components/Tutorial/Tutorial.types'; import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, initErrorDataState, } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; +import Text from 'components/Text/Text'; +import Tutorial from 'components/Tutorial/Tutorial'; +import { TutorialStep } from 'components/Tutorial/Tutorial.types'; import AlertReceiveChannelCard from 'containers/AlertReceiveChannelCard/AlertReceiveChannelCard'; import AlertRules from 'containers/AlertRules/AlertRules'; import CreateAlertReceiveChannelContainer from 'containers/CreateAlertReceiveChannelContainer/CreateAlertReceiveChannelContainer'; diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index e48f10aa..8ddd846c 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -7,14 +7,14 @@ import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import GTable from 'components/GTable/GTable'; -import PluginLink from 'components/PluginLink/PluginLink'; -import Text from 'components/Text/Text'; -import WithConfirm from 'components/WithConfirm/WithConfirm'; import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, initErrorDataState, } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; +import PluginLink from 'components/PluginLink/PluginLink'; +import Text from 'components/Text/Text'; +import WithConfirm from 'components/WithConfirm/WithConfirm'; import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookForm'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { ActionDTO } from 'models/action'; diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 5da63196..230d2c84 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -21,17 +21,17 @@ import moment from 'moment-timezone'; import instructionsImage from 'assets/img/events_instructions.png'; import Avatar from 'components/Avatar/Avatar'; import GTable from 'components/GTable/GTable'; +import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; +import { + getWrongTeamResponseInfo, + initErrorDataState, +} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import PluginLink from 'components/PluginLink/PluginLink'; import SchedulesFilters from 'components/SchedulesFilters/SchedulesFilters'; import { SchedulesFiltersType } from 'components/SchedulesFilters/SchedulesFilters.types'; import Text from 'components/Text/Text'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; -import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; -import { - getWrongTeamResponseInfo, - initErrorDataState, -} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import ScheduleForm from 'containers/ScheduleForm/ScheduleForm'; import ScheduleICalSettings from 'containers/ScheduleIcalLink/ScheduleIcalLink'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index ffdc92c4..3e75aacd 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -9,14 +9,14 @@ import { observer } from 'mobx-react'; import Avatar from 'components/Avatar/Avatar'; import GTable from 'components/GTable/GTable'; -import PluginLink from 'components/PluginLink/PluginLink'; -import Text from 'components/Text/Text'; -import UsersFilters from 'components/UsersFilters/UsersFilters'; import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, initErrorDataState, } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; +import PluginLink from 'components/PluginLink/PluginLink'; +import Text from 'components/Text/Text'; +import UsersFilters from 'components/UsersFilters/UsersFilters'; import UserSettings from 'containers/UserSettings/UserSettings'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { getRole } from 'models/user/user.helpers'; From 645b78d033fed47aea4077d78bb4715a90e82b4d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 26 Sep 2022 14:17:17 +0300 Subject: [PATCH 47/89] conditional useStore --- .../PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx index f53193bd..5d0445ba 100644 --- a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx +++ b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx @@ -46,10 +46,10 @@ export default function PageErrorHandlingWrapper({ } }, [errorData.isNotFoundError]); - if (!errorData.isWrongTeamError) {return children();} - const store = useStore(); + if (!errorData.isWrongTeamError) {return children();} + const currentTeamId = store.userStore.currentUser?.current_team; const currentTeam = store.grafanaTeamStore.items[currentTeamId]?.name; From 516b2c446a8fe34383ff5bff1f130c5c913edb0e Mon Sep 17 00:00:00 2001 From: Tim Willems Date: Tue, 27 Sep 2022 17:20:41 +0200 Subject: [PATCH 48/89] Changed Redis Connection String possibilities (#538) * Update helm.py Changed Redis Connection String possibilities added Redis Username added Redis Protocol Co-authored-by: Joey Orlando --- engine/settings/helm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/settings/helm.py b/engine/settings/helm.py index 3f9f1a62..00fa96c3 100644 --- a/engine/settings/helm.py +++ b/engine/settings/helm.py @@ -33,10 +33,12 @@ CELERY_BROKER_URL = ( f"{RABBITMQ_PROTOCOL}://{RABBITMQ_USERNAME}:{RABBITMQ_PASSWORD}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST}" ) +REDIS_USERNAME = os.environ.get("REDIS_USERNAME", "") REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD") REDIS_HOST = os.environ.get("REDIS_HOST") REDIS_PORT = os.environ.get("REDIS_PORT", "6379") -REDIS_URI = f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}" +REDIS_PROTOCOL = os.environ.get("REDIS_PROTOCOL", "redis") +REDIS_URI = f"{REDIS_PROTOCOL}://{REDIS_USERNAME}:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}" CACHES = { "default": { From 0afb377117f451873e016c7b78ccc9ba5978b891 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Tue, 27 Sep 2022 17:31:25 +0200 Subject: [PATCH 49/89] Feat 380/311 - make plugin configuration error messages more human readable/actionable (#563) * remove unused imports * remove unused/redundant jsx comment * don't show pluginStatusMessage as a styled link * show more human readable plugin configuration errors --- README.md | 22 +++++---- .../PluginConfigPage/PluginConfigPage.tsx | 47 +++++++++++-------- .../containers/PluginConfigPage/helpers.tsx | 5 ++ 3 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 grafana-plugin/src/containers/PluginConfigPage/helpers.tsx diff --git a/README.md b/README.md index f6c0e447..1eea669c 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,13 @@ Developer-friendly incident response with brilliant Slack integration. We prepared multiple environments: [production](https://grafana.com/docs/grafana-cloud/oncall/open-source/#production-environment), [developer](DEVELOPER.md) and hobby: 1. Download docker-compose.yaml: + ```bash curl -fsSL https://raw.githubusercontent.com/grafana/oncall/dev/docker-compose.yml -o docker-compose.yml ``` 2. Set variables: + ```bash echo "DOMAIN=http://localhost:8080 SECRET_KEY=my_random_secret_must_be_more_than_32_characters_long @@ -37,26 +39,31 @@ GRAFANA_PASSWORD=admin" > .env_hobby ``` 3. Launch services: + ```bash docker-compose --env-file .env_hobby -f docker-compose.yml up -d ``` 4. Issue one-time invite token: + ```bash docker-compose --env-file .env_hobby -f docker-compose.yml run engine python manage.py issue_invite_for_the_frontend --override ``` +**Note**: if you remove the plugin configuration and reconfigure it, you will need to generate a new one-time invite token for your new configuration. + 5. Go to [OnCall Plugin Configuration](http://localhost:3000/plugins/grafana-oncall-app), using log in credentials as defined above: `admin`/`admin` (or find OnCall plugin in configuration->plugins) and connect OnCall _plugin_ with OnCall _backend_: + ``` Invite token: ^^^ from the previous step. OnCall backend URL: http://engine:8080 Grafana Url: http://grafana:3000 ``` -6. Enjoy! Check our [OSS docs](https://grafana.com/docs/grafana-cloud/oncall/open-source/) if you want to set up Slack, Telegram, Twilio or SMS/calls through Grafana Cloud. - +6. Enjoy! Check our [OSS docs](https://grafana.com/docs/grafana-cloud/oncall/open-source/) if you want to set up Slack, Telegram, Twilio or SMS/calls through Grafana Cloud. ## Update version + To update your Grafana OnCall hobby environment: ```shell @@ -76,14 +83,13 @@ See [Grafana docs](https://grafana.com/docs/grafana/latest/administration/plugin - ## Stargazers over time [![Stargazers over time](https://starchart.cc/grafana/oncall.svg)](https://starchart.cc/grafana/oncall) - ## Further Reading -- *Migration from the PagerDuty* - [Migrator](https://github.com/grafana/oncall/tree/dev/tools/pagerduty-migrator) -- *Documentation* - [Grafana OnCall](https://grafana.com/docs/grafana-cloud/oncall/) -- *Blog Post* - [Announcing Grafana OnCall, the easiest way to do on-call management](https://grafana.com/blog/2021/11/09/announcing-grafana-oncall/) -- *Presentation* - [Deep dive into the Grafana, Prometheus, and Alertmanager stack for alerting and on-call management](https://grafana.com/go/observabilitycon/2021/alerting/?pg=blog) + +- _Migration from the PagerDuty_ - [Migrator](https://github.com/grafana/oncall/tree/dev/tools/pagerduty-migrator) +- _Documentation_ - [Grafana OnCall](https://grafana.com/docs/grafana-cloud/oncall/) +- _Blog Post_ - [Announcing Grafana OnCall, the easiest way to do on-call management](https://grafana.com/blog/2021/11/09/announcing-grafana-oncall/) +- _Presentation_ - [Deep dive into the Grafana, Prometheus, and Alertmanager stack for alerting and on-call management](https://grafana.com/go/observabilitycon/2021/alerting/?pg=blog) diff --git a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx index 264315b3..e199bf02 100644 --- a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx +++ b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx @@ -11,12 +11,8 @@ import { Label, Legend, LoadingPlaceholder, - Icon, - Alert, - Modal, } from '@grafana/ui'; import cn from 'classnames/bind'; -import CopyToClipboard from 'react-copy-to-clipboard'; import { OnCallAppSettings } from 'types'; import Block from 'components/GBlock/Block'; @@ -28,6 +24,8 @@ import { createGrafanaToken, getPluginSyncStatus, startPluginSync, updateGrafana import { GRAFANA_LICENSE_OSS } from 'utils/consts'; import { getItem, setItem } from 'utils/localStorage'; +import { constructSyncErrorMessage, constructErrorActionMessage } from './helpers'; + import styles from './PluginConfigPage.module.css'; const cx = cn.bind(styles); @@ -45,6 +43,8 @@ export const PluginConfigPage = (props: Props) => { const [isSelfHostedInstall, setIsSelfHostedInstall] = useState(true); const [retrySync, setRetrySync] = useState(false); + const INVALID_INVITE_TOKEN_ERROR_MSG = `It seems like your invite token may be invalid. ${constructErrorActionMessage('generating a new invite token')}`; + const setupPlugin = useCallback(async () => { setItem('onCallApiUrl', onCallApiUrl); setItem('grafanaUrl', grafanaUrl); @@ -129,25 +129,37 @@ export const PluginConfigPage = (props: Props) => { }, []); const handleSyncException = useCallback((e) => { + const buildErrMsg = (msg: string): string => + constructSyncErrorMessage(msg, plugin.meta.jsonData.onCallApiUrl); + if (plugin.meta.jsonData?.onCallApiUrl) { - let statusMessage = plugin.meta.jsonData.onCallApiUrl + '\n' + e + ', retry or check settings & re-initialize.'; - if (e.response.status == 404) { - statusMessage += '\nIf Grafana OnCall was just installed, restart Grafana for OnCall routes to be available.'; + const { status: statusCode } = e.response; + + let statusMessage: string; + + if (statusCode == 403) { + statusMessage = buildErrMsg(INVALID_INVITE_TOKEN_ERROR_MSG); + } else if (statusCode === 404) { + statusMessage = buildErrMsg('If Grafana OnCall was just installed, restart Grafana for OnCall routes to be available.'); + } else if (statusCode === 502) { + statusMessage = buildErrMsg(`Unable to communicate with either the Grafana API, or Grafana OnCall engine API. ${constructErrorActionMessage('verify that the API URLs that you entered are correct')}`); + } else { + statusMessage = buildErrMsg(`An unknown error occured. ${constructErrorActionMessage()}. If the error still occurs please reach out to support.`) } setPluginStatusMessage(statusMessage); setRetrySync(true); } else { - setPluginStatusMessage('OnCall has not been setup, configure & initialize below.'); + setPluginStatusMessage(buildErrMsg('OnCall has not been setup, configure & initialize below.')); } setPluginStatusOk(false); setPluginConfigLoading(false); }, []); - const finishSync = useCallback((get_sync_response) => { - if (get_sync_response.token_ok) { + const finishSync = useCallback((getSyncResponse) => { + if (getSyncResponse.token_ok) { const versionInfo = - get_sync_response.version && get_sync_response.license - ? ` (${get_sync_response.license}, ${get_sync_response.version})` + getSyncResponse.version && getSyncResponse.license + ? ` (${getSyncResponse.license}, ${getSyncResponse.version})` : ''; let pluginStatusMessage = `Connected to OnCall${versionInfo}\n - OnCall URL: ${plugin.meta.jsonData.onCallApiUrl}\n` @@ -159,9 +171,8 @@ export const PluginConfigPage = (props: Props) => { setIsSelfHostedInstall(plugin.meta.jsonData?.license === GRAFANA_LICENSE_OSS); setPluginStatusOk(true); } else { - setPluginStatusMessage( - `OnCall failed to connect to this grafana via: ${plugin.meta.jsonData.grafanaUrl} check URL, network, and API key.` - ); + setPluginStatusMessage(constructSyncErrorMessage(INVALID_INVITE_TOKEN_ERROR_MSG, + plugin.meta.jsonData.grafanaUrl)); setRetrySync(true); } setPluginConfigLoading(false); @@ -221,14 +232,10 @@ export const PluginConfigPage = (props: Props) => { )}

{'Plugin <-> backend connection status'}

-            {pluginStatusMessage}
+            {pluginStatusMessage}
           
- {/*

{'Plugin <-> backend connection status'}

-
-                {pluginStatusMessage}
-              
*/} {retrySync && ( - + From 20b8dbc2fd216568b055950fd924c67864676400 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 29 Sep 2022 08:45:15 +0100 Subject: [PATCH 54/89] lint --- .../src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx index aaeda4a0..b2984341 100644 --- a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx +++ b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx @@ -40,7 +40,7 @@ const GrafanaTeamSelect = observer((props: GrafanaTeamSelectProps) => { window.location.search = queryParams.toString(); function mapCurrentPage() { - if (currentPage === 'incident') return 'incidents' + if (currentPage === 'incident') {return 'incidents'} return currentPage } }; From 72e3a8c1a7ff2800284bb08e81029a8a4741e5ef Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 29 Sep 2022 12:09:29 +0100 Subject: [PATCH 55/89] Update docker-compose version to 3.8 (#577) * update docker-compose to 3.8 + cleanup * change single quote to double * make cpus string * use extension field for oncall environment --- docker-compose-developer-pg.yml | 53 +++++++++------ docker-compose-developer.yml | 41 +++++++----- docker-compose.yml | 114 ++++++++++++++------------------ 3 files changed, 108 insertions(+), 100 deletions(-) diff --git a/docker-compose-developer-pg.yml b/docker-compose-developer-pg.yml index f6f813f2..f42f17e3 100644 --- a/docker-compose-developer-pg.yml +++ b/docker-compose-developer-pg.yml @@ -1,52 +1,62 @@ -version: '3.2' +version: "3.8" services: - postgres: image: postgres:14.4 - platform: linux/x86_64 - mem_limit: 500m - cpus: 0.5 restart: always ports: - - 5432:5432 + - "5432:5432" environment: POSTGRES_DB: oncall_local_dev POSTGRES_PASSWORD: empty - POSTGRES_INITDB_ARGS: '--encoding=UTF-8' + POSTGRES_INITDB_ARGS: --encoding=UTF-8 + deploy: + resources: + limits: + memory: 500m + cpus: '0.5' redis: image: redis - mem_limit: 100m - cpus: 0.1 restart: always ports: - - 6379:6379 + - "6379:6379" + deploy: + resources: + limits: + memory: 100m + cpus: '0.1' rabbit: image: "rabbitmq:3.7.15-management" - mem_limit: 1000m - cpus: 0.5 environment: RABBITMQ_DEFAULT_USER: "rabbitmq" RABBITMQ_DEFAULT_PASS: "rabbitmq" RABBITMQ_DEFAULT_VHOST: "/" + deploy: + resources: + limits: + memory: 1000m + cpus: '0.5' ports: - - 15672:15672 - - 5672:5672 + - "15672:15672" + - "5672:5672" mysql-to-create-grafana-db: image: mysql:5.7 platform: linux/x86_64 - mem_limit: 500m - cpus: 0.5 command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci restart: always ports: - - 3306:3306 + - "3306:3306" environment: MYSQL_ROOT_PASSWORD: empty MYSQL_DATABASE: grafana + deploy: + resources: + limits: + memory: 500m + cpus: '0.5' healthcheck: test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] timeout: 20s @@ -55,8 +65,6 @@ services: grafana: image: "grafana/grafana:main" restart: always - mem_limit: 500m - cpus: 0.5 environment: GF_DATABASE_TYPE: mysql GF_DATABASE_HOST: mysql @@ -65,10 +73,15 @@ services: GF_SECURITY_ADMIN_USER: oncall GF_SECURITY_ADMIN_PASSWORD: oncall GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-oncall-app + deploy: + resources: + limits: + memory: 500m + cpus: '0.5' volumes: - ./grafana-plugin:/var/lib/grafana/plugins/grafana-plugin ports: - - 3000:3000 + - "3000:3000" depends_on: mysql-to-create-grafana-db: condition: service_healthy diff --git a/docker-compose-developer.yml b/docker-compose-developer.yml index dc2f1179..33ef3fd1 100644 --- a/docker-compose-developer.yml +++ b/docker-compose-developer.yml @@ -1,19 +1,21 @@ -version: '3.2' +version: "3.8" services: - mysql: image: mysql:5.7 platform: linux/x86_64 - mem_limit: 500m - cpus: 0.5 command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci restart: always ports: - - 3306:3306 + - "3306:3306" environment: MYSQL_ROOT_PASSWORD: empty MYSQL_DATABASE: oncall_local_dev + deploy: + resources: + limits: + memory: 500m + cpus: '0.5' healthcheck: test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] timeout: 20s @@ -21,23 +23,29 @@ services: redis: image: redis - mem_limit: 100m - cpus: 0.1 restart: always ports: - - 6379:6379 + - "6379:6379" + deploy: + resources: + limits: + memory: 100m + cpus: '0.1' rabbit: image: "rabbitmq:3.7.15-management" - mem_limit: 1000m - cpus: 0.5 environment: RABBITMQ_DEFAULT_USER: "rabbitmq" RABBITMQ_DEFAULT_PASS: "rabbitmq" RABBITMQ_DEFAULT_VHOST: "/" + deploy: + resources: + limits: + memory: 1000m + cpus: '0.5' ports: - - 15672:15672 - - 5672:5672 + - "15672:15672" + - "5672:5672" mysql-to-create-grafana-db: image: mysql:5.7 @@ -50,8 +58,6 @@ services: grafana: image: "grafana/grafana:main" restart: always - mem_limit: 500m - cpus: 0.5 environment: GF_DATABASE_TYPE: mysql GF_DATABASE_HOST: mysql @@ -60,10 +66,15 @@ services: GF_SECURITY_ADMIN_USER: oncall GF_SECURITY_ADMIN_PASSWORD: oncall GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-oncall-app + deploy: + resources: + limits: + memory: 500m + cpus: '0.5' volumes: - ./grafana-plugin:/var/lib/grafana/plugins/grafana-plugin ports: - - 3000:3000 + - "3000:3000" depends_on: mysql: condition: service_healthy diff --git a/docker-compose.yml b/docker-compose.yml index 9caaac8a..45ae8207 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,28 +1,36 @@ +version: "3.8" + +x-environment: + &oncall-environment + BASE_URL: $DOMAIN + SECRET_KEY: $SECRET_KEY + RABBITMQ_USERNAME: "rabbitmq" + RABBITMQ_PASSWORD: $RABBITMQ_PASSWORD + RABBITMQ_HOST: "rabbitmq" + RABBITMQ_PORT: "5672" + RABBITMQ_DEFAULT_VHOST: "/" + MYSQL_PASSWORD: $MYSQL_PASSWORD + MYSQL_DB_NAME: oncall_hobby + MYSQL_USER: ${MYSQL_USER:-root} + MYSQL_HOST: ${MYSQL_HOST:-mysql} + MYSQL_PORT: 3306 + REDIS_URI: redis://redis:6379/0 + DJANGO_SETTINGS_MODULE: settings.hobby + CELERY_WORKER_QUEUE: "default,critical,long,slack,telegram,webhook,retry,celery" + CELERY_WORKER_CONCURRENCY: "1" + CELERY_WORKER_MAX_TASKS_PER_CHILD: "100" + CELERY_WORKER_SHUTDOWN_INTERVAL: "65m" + CELERY_WORKER_BEAT_ENABLED: "True" + services: engine: image: grafana/oncall restart: always ports: - - 8080:8080 + - "8080:8080" command: > sh -c "uwsgi --ini uwsgi.ini" - environment: - BASE_URL: $DOMAIN - SECRET_KEY: $SECRET_KEY - RABBITMQ_USERNAME: "rabbitmq" - RABBITMQ_PASSWORD: $RABBITMQ_PASSWORD - RABBITMQ_HOST: "rabbitmq" - RABBITMQ_PORT: "5672" - RABBITMQ_DEFAULT_VHOST: "/" - MYSQL_PASSWORD: $MYSQL_PASSWORD - MYSQL_DB_NAME: oncall_hobby - MYSQL_USER: ${MYSQL_USER:-root} - MYSQL_HOST: ${MYSQL_HOST:-mysql} - MYSQL_PORT: 3306 - REDIS_URI: redis://redis:6379/0 - DJANGO_SETTINGS_MODULE: settings.hobby - OSS: "True" - CELERY_WORKER_QUEUE: "default,critical,long,slack,telegram,webhook,retry,celery" + environment: *oncall-environment depends_on: mysql: condition: service_healthy @@ -37,27 +45,7 @@ services: image: grafana/oncall restart: always command: sh -c "./celery_with_exporter.sh" - environment: - BASE_URL: $DOMAIN - SECRET_KEY: $SECRET_KEY - RABBITMQ_USERNAME: "rabbitmq" - RABBITMQ_PASSWORD: $RABBITMQ_PASSWORD - RABBITMQ_HOST: "rabbitmq" - RABBITMQ_PORT: "5672" - RABBITMQ_DEFAULT_VHOST: "/" - MYSQL_PASSWORD: $MYSQL_PASSWORD - MYSQL_DB_NAME: oncall_hobby - MYSQL_USER: ${MYSQL_USER:-root} - MYSQL_HOST: ${MYSQL_HOST:-mysql} - MYSQL_PORT: 3306 - REDIS_URI: redis://redis:6379/0 - DJANGO_SETTINGS_MODULE: settings.hobby - OSS: "True" - CELERY_WORKER_QUEUE: "default,critical,long,slack,telegram,webhook,retry,celery" - CELERY_WORKER_CONCURRENCY: "1" - CELERY_WORKER_MAX_TASKS_PER_CHILD: "100" - CELERY_WORKER_SHUTDOWN_INTERVAL: "65m" - CELERY_WORKER_BEAT_ENABLED: "True" + environment: *oncall-environment depends_on: mysql: condition: service_healthy @@ -71,23 +59,7 @@ services: oncall_db_migration: image: grafana/oncall command: python manage.py migrate --noinput - environment: - BASE_URL: $DOMAIN - SECRET_KEY: $SECRET_KEY - RABBITMQ_USERNAME: "rabbitmq" - RABBITMQ_PASSWORD: $RABBITMQ_PASSWORD - RABBITMQ_HOST: "rabbitmq" - RABBITMQ_PORT: "5672" - RABBITMQ_DEFAULT_VHOST: "/" - MYSQL_PASSWORD: $MYSQL_PASSWORD - MYSQL_DB_NAME: oncall_hobby - MYSQL_USER: ${MYSQL_USER:-root} - MYSQL_HOST: ${MYSQL_HOST:-mysql} - MYSQL_PORT: 3306 - REDIS_URI: redis://redis:6379/0 - DJANGO_SETTINGS_MODULE: settings.hobby - OSS: "True" - CELERY_WORKER_QUEUE: "default,critical,long,slack,telegram,webhook,retry,celery" + environment: *oncall-environment depends_on: mysql: condition: service_healthy @@ -97,8 +69,6 @@ services: mysql: image: mysql:5.7 platform: linux/x86_64 - mem_limit: 500m - cpus: 0.5 command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci restart: always expose: @@ -108,6 +78,11 @@ services: environment: MYSQL_ROOT_PASSWORD: $MYSQL_PASSWORD MYSQL_DATABASE: oncall_hobby + deploy: + resources: + limits: + memory: 500m + cpus: '0.5' healthcheck: test: "mysql -uroot -p$MYSQL_PASSWORD oncall_hobby -e 'select 1'" timeout: 20s @@ -115,24 +90,30 @@ services: redis: image: redis - mem_limit: 100m - cpus: 0.1 restart: always expose: - 6379 + deploy: + resources: + limits: + memory: 100m + cpus: '0.1' rabbitmq: image: "rabbitmq:3.7.15-management" restart: always hostname: rabbitmq - mem_limit: 1000m - cpus: 0.5 volumes: - rabbitmqdata:/var/lib/rabbitmq environment: RABBITMQ_DEFAULT_USER: "rabbitmq" RABBITMQ_DEFAULT_PASS: $RABBITMQ_PASSWORD RABBITMQ_DEFAULT_VHOST: "/" + deploy: + resources: + limits: + memory: 1000m + cpus: '0.5' healthcheck: test: rabbitmq-diagnostics -q ping interval: 30s @@ -152,10 +133,8 @@ services: grafana: image: "grafana/grafana:9.0.0-beta3" restart: always - mem_limit: 500m ports: - - 3000:3000 - cpus: 0.5 + - "3000:3000" environment: GF_DATABASE_TYPE: mysql GF_DATABASE_HOST: ${MYSQL_HOST:-mysql} @@ -165,6 +144,11 @@ services: GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:?err} GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-oncall-app GF_INSTALL_PLUGINS: grafana-oncall-app + deploy: + resources: + limits: + memory: 500m + cpus: '0.5' depends_on: mysql_to_create_grafana_db: condition: service_completed_successfully From 526b04f71e430c64d53937a33998c94e10f48b1c Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 29 Sep 2022 12:09:54 +0100 Subject: [PATCH 56/89] change alert group naming for demo alert link (#571) --- grafana-plugin/src/containers/AlertRules/AlertRules.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/grafana-plugin/src/containers/AlertRules/AlertRules.tsx b/grafana-plugin/src/containers/AlertRules/AlertRules.tsx index 5c4d5996..10f48d9d 100644 --- a/grafana-plugin/src/containers/AlertRules/AlertRules.tsx +++ b/grafana-plugin/src/containers/AlertRules/AlertRules.tsx @@ -804,8 +804,8 @@ class AlertRules extends React.Component { alertReceiveChannelStore.updateCounters(); openNotification(
- Demo alert was generated. Find it in the - "Incidents" + Demo alert was generated. Find it on the + "Alert Groups" page and make sure it didn't freak out your colleagues 😉
); @@ -821,8 +821,8 @@ class AlertRules extends React.Component { alertReceiveChannelStore.sendDemoAlertToParticularRoute(id).then(() => { openNotification(
- Demo alert was generated. Find it in the - "Incidents" + Demo alert was generated. Find it on the + "Alert Groups" page and make sure it didn't freak out your colleagues 😉
); From 22cb36bbb2da92d00776376345e9b22e99146075 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 29 Sep 2022 13:06:33 +0100 Subject: [PATCH 57/89] Simplify README.md commands (#578) * .env_hobby -> .env * update instructions * pull only engine * spelling * remove GRAFANA_USER and GRAFANA_PASSWORD * .env -> .env.dev --- .env.example => .env.dev.example | 0 .gitignore | 1 + DEVELOPER.md | 20 ++++++++++---------- README.md | 16 +++++++--------- docker-compose.yml | 2 +- 5 files changed, 19 insertions(+), 20 deletions(-) rename .env.example => .env.dev.example (100%) diff --git a/.env.example b/.env.dev.example similarity index 100% rename from .env.example rename to .env.dev.example diff --git a/.gitignore b/.gitignore index cadd75d3..308f671f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ venv .python-version .env .env_hobby +.env.dev .vscode dump.rdb .idea diff --git a/DEVELOPER.md b/DEVELOPER.md index da536813..fd71c96b 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -56,15 +56,15 @@ python --version # Make sure you have latest pip and wheel support pip install -U pip wheel -# Copy and check .env file. -cp .env.example .env +# Copy and check .env.dev file. +cp .env.dev.example .env.dev -# NOTE: if you want to use the PostgreSQL db backend add DB_BACKEND=postgresql to your .env file; +# NOTE: if you want to use the PostgreSQL db backend add DB_BACKEND=postgresql to your .env.dev file; # currently allowed backend values are `mysql` (default) and `postgresql` -# Apply .env to current terminal. +# Apply .env.dev to current terminal. # For PyCharm it's better to use https://plugins.jetbrains.com/plugin/7861-envfile/ -export $(grep -v '^#' .env | xargs -0) +export $(grep -v '^#' .env.dev | xargs -0) # Install dependencies. # Hint: there is a known issue with uwsgi. It's not used in the local dev environment. Feel free to comment it in `engine/requirements.txt`. @@ -83,7 +83,7 @@ python manage.py createsuperuser # Http server: python manage.py runserver 0.0.0.0:8080 -# Worker for background tasks (run it in the parallel terminal, don't forget to export .env there) +# Worker for background tasks (run it in the parallel terminal, don't forget to export .env.dev there) python manage.py start_celery # Additionally you could launch the worker with periodic tasks launcher (99% you don't need this) @@ -248,7 +248,7 @@ Credentials: admin/admin ### Running tests locally -In the `engine` directory, with the `.env` vars exported and virtualenv activated +In the `engine` directory, with the `.env.dev` vars exported and virtualenv activated ```bash pytest @@ -265,10 +265,10 @@ pytest -n4 ### PyCharm -1. Create venv and copy .env file +1. Create venv and copy .env.dev file ```bash python3.9 -m venv venv - cp .env.example .env + cp .env.dev.example .env.dev ``` 2. Open the project in PyCharm 3. Settings → Project OnCall @@ -279,5 +279,5 @@ pytest -n4 - Set Django project root to /engine - Set Settings to settings/dev.py 5. Create a new Django Server run configuration to Run/Debug the engine - - Use a plugin such as EnvFile to load the .env file + - Use a plugin such as EnvFile to load the .env.dev file - Change port from 8000 to 8080 diff --git a/README.md b/README.md index 1eea669c..51eaa6b1 100644 --- a/README.md +++ b/README.md @@ -30,24 +30,22 @@ curl -fsSL https://raw.githubusercontent.com/grafana/oncall/dev/docker-compose.y ```bash echo "DOMAIN=http://localhost:8080 +COMPOSE_PROFILES=with_grafana # Remove this line if you want to use existing grafana SECRET_KEY=my_random_secret_must_be_more_than_32_characters_long RABBITMQ_PASSWORD=rabbitmq_secret_pw -MYSQL_PASSWORD=mysql_secret_pw -COMPOSE_PROFILES=with_grafana # Remove this line if you want to use existing grafana -GRAFANA_USER=admin -GRAFANA_PASSWORD=admin" > .env_hobby +MYSQL_PASSWORD=mysql_secret_pw" > .env ``` 3. Launch services: ```bash -docker-compose --env-file .env_hobby -f docker-compose.yml up -d +docker-compose up -d ``` 4. Issue one-time invite token: ```bash -docker-compose --env-file .env_hobby -f docker-compose.yml run engine python manage.py issue_invite_for_the_frontend --override +docker-compose run engine python manage.py issue_invite_for_the_frontend --override ``` **Note**: if you remove the plugin configuration and reconfigure it, you will need to generate a new one-time invite token for your new configuration. @@ -67,11 +65,11 @@ Grafana Url: http://grafana:3000 To update your Grafana OnCall hobby environment: ```shell -# Update Docker images -docker-compose --env-file .env_hobby -f docker-compose.yml pull engine celery oncall_db_migration +# Update Docker image +docker-compose pull engine # Re-deploy -docker-compose --env-file .env_hobby -f docker-compose.yml up -d --remove-orphans +docker-compose up -d ``` After updating the engine, you'll also need to click the "Update" button on the [plugin version page](http://localhost:3000/plugins/grafana-oncall-app?page=version-history). diff --git a/docker-compose.yml b/docker-compose.yml index 45ae8207..a77f5d25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -141,7 +141,7 @@ services: GF_DATABASE_USER: ${MYSQL_USER:-root} GF_DATABASE_PASSWORD: ${MYSQL_PASSWORD:?err} GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin} - GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:?err} + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin} GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-oncall-app GF_INSTALL_PLUGINS: grafana-oncall-app deploy: From db861244270329845e7279d91cb7ab04520460a2 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 29 Sep 2022 17:38:39 +0300 Subject: [PATCH 58/89] reload incidents every 15s + some cleanup --- .../IncidentsFilters/IncidentsFilters.tsx | 16 +-- .../src/models/alertgroup/alertgroup.ts | 12 +- .../src/pages/incidents/Incidents.tsx | 103 ++++-------------- 3 files changed, 32 insertions(+), 99 deletions(-) diff --git a/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx b/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx index f0c3b74c..eb8e52ba 100644 --- a/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx +++ b/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx @@ -77,9 +77,7 @@ class IncidentsFilters extends Component { - this.onChange(true); - }); + this.setState({ filterOptions, filters, values }, () => this.onChange(true)); } render() { @@ -92,11 +90,8 @@ class IncidentsFilters extends Component { - const { store, value } = this.props; const { filterOptions, filters } = this.state; - const filterNames = filters.map((filter: FilterOption) => filter.name); - if (!filterOptions) { return ; } @@ -234,8 +229,7 @@ class IncidentsFilters extends Component { - const { value, onChange } = this.props; - const { values, filters } = this.state; + const { filters } = this.state; this.setState({ filters: [...filters, option.data], @@ -250,7 +244,7 @@ class IncidentsFilters extends Component { - const { values, filterOptions, hadInteraction } = this.state; + const { values, hadInteraction } = this.state; const autoFocus = Boolean(hadInteraction); @@ -342,7 +336,6 @@ class IncidentsFilters extends Component { - const { store } = this.props; return (selected: boolean) => { const { values } = this.state; @@ -406,7 +399,6 @@ class IncidentsFilters extends Component { - const { onChange } = this.props; const { values } = this.state; const newValues = omitBy({ ...values, [name]: value }, isUndefined); @@ -425,6 +417,8 @@ class IncidentsFilters extends Component { @@ -69,7 +70,7 @@ class Incidents extends React.Component const { store, - query: { id, cursor: cursorQuery, start: startQuery, perpage: perpageQuery }, + query: { cursor: cursorQuery, start: startQuery, perpage: perpageQuery }, } = props; const cursor = cursorQuery || undefined; @@ -92,6 +93,13 @@ class Incidents extends React.Component store.alertGroupStore.updateSilenceOptions(); } + private pollingIntervalId: NodeJS.Timer = undefined; + + componentWillUnmount(): void { + clearInterval(this.pollingIntervalId); + this.pollingIntervalId = undefined; + } + render() { return (
@@ -128,9 +136,18 @@ class Incidents extends React.Component }); } - store.alertGroupStore.updateIncidentFilters(filters, isOnMount); + const fetchIncidentData = () => { + store.alertGroupStore.updateIncidentFilters(filters, isOnMount); // this line fetches incidents + getLocationSrv().update({ query: { page: 'incidents', ...store.alertGroupStore.incidentFilters } }); + } - getLocationSrv().update({ query: { page: 'incidents', ...store.alertGroupStore.incidentFilters } }); + if (this.pollingIntervalId) { + clearInterval(this.pollingIntervalId); + } + + this.pollingIntervalId = setInterval(() => fetchIncidentData(), POLLING_NUM_SECONDS * 1000); + + fetchIncidentData(); }; onChangeCursor = (cursor: string, direction: 'prev' | 'next') => { @@ -211,31 +228,6 @@ class Incidents extends React.Component )} - - {/* {store.alertGroupStore.bulkActions.map((bulkAction: SelectOption) => { - if (bulkAction.value === 'silence') { - return ( - - ); - } - - return ( - - - - ); - })}*/} {hasSelected ? `${selectedIncidentIds.length} alert group${selectedIncidentIds.length > 1 ? 's' : ''} selected` @@ -261,11 +253,8 @@ class Incidents extends React.Component }; renderTable() { - const { selectedIncidentIds, affectedRows, pagination } = this.state; + const { selectedIncidentIds, pagination } = this.state; const { store } = this.props; - const { - teamStore: { currentTeam }, - } = store; const { alertGroupsLoading } = store.alertGroupStore; const results = store.alertGroupStore.getAlertSearchResult('default'); @@ -344,12 +333,6 @@ class Incidents extends React.Component }, ]; - const loading = store.alertGroupStore.alertGroupsLoading; - - const hasInvalidatedAlert = Boolean( - (results && results.some((alert: AlertType) => alert.undoAction)) || Object.keys(affectedRows).length - ); - return (
{this.renderBulkActions()} @@ -359,14 +342,8 @@ class Incidents extends React.Component className={cx('incidents-table')} rowSelection={{ selectedRowKeys: selectedIncidentIds, onChange: this.handleSelectedIncidentIdsChange }} rowKey="pk" - /*title={() => ( - - Incidents - - )}*/ data={results} columns={columns} - // rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)} />
)} ); - - /* if (record.resolved_by_user) { - const index = users.findIndex((user) => user.pk === record.resolved_by_user.pk); - if (index > -1) { - users = move(users, index, 0); - } - } - - if (record.acknowledged_by_user) { - const index = users.findIndex((user) => user.pk === record.acknowledged_by_user.pk); - if (index > -1) { - users = move(users, index, 0); - } - } - - return ( - - {users.map((user: User) => { - let badge = undefined; - if (record.resolved_by_user && user.pk === record.resolved_by_user.pk) { - badge = ; - } else if (record.acknowledged_by_user && user.pk === record.acknowledged_by_user.pk) { - badge = ; - } - - return ( - - - - } /> - - - - ); - })} - - ); */ }; renderActionButtons = (incident: AlertType) => { @@ -571,7 +511,6 @@ class Incidents extends React.Component getUnsilenceClickHandler = (alert: AlertType) => { const { store } = this.props; - const { alertGroupStore } = store; return (event: any) => { event.stopPropagation(); From 1351e7e1ecedb6842a65150489768f612f77a87b Mon Sep 17 00:00:00 2001 From: Maxim Mordasov Date: Fri, 30 Sep 2022 17:28:17 +0300 Subject: [PATCH 59/89] implement light theme for new schedules (#585) --- .../src/components/Modal/Modal.module.css | 6 ++--- .../src/components/Table/Table.module.css | 6 ++--- grafana-plugin/src/components/Table/Table.tsx | 2 +- grafana-plugin/src/components/Text/Text.tsx | 6 +++-- .../TimelineMarks/TimelineMarks.module.css | 1 - .../TimelineMarks/TimelineMarks.tsx | 8 +++++-- .../UserGroups/UserGroups.module.css | 13 +++++++---- .../src/components/UserGroups/UserGroups.tsx | 11 +++++---- .../UserTimezoneSelect/UserTimezoneSelect.tsx | 2 +- .../containers/Rotation/Rotation.module.css | 2 +- .../containers/RotationForm/RotationForm.tsx | 2 +- .../RotationForm/ScheduleOverrideForm.tsx | 2 +- .../containers/Rotations/Rotations.module.css | 17 ++++---------- .../src/containers/Rotations/Rotations.tsx | 16 ++++++++----- .../containers/Rotations/ScheduleFinal.tsx | 7 +++++- .../Rotations/ScheduleOverrides.tsx | 7 +++++- .../containers/ScheduleSlot/ScheduleSlot.tsx | 4 ++-- .../UsersTimezones/UsersTimezones.module.css | 10 ++++---- .../UsersTimezones/UsersTimezones.tsx | 16 ++++++++++--- grafana-plugin/src/icons/index.tsx | 2 +- grafana-plugin/src/models/user/user.ts | 2 +- .../src/pages/schedule/Schedule.module.css | 2 +- .../src/pages/schedule/Schedule.tsx | 6 ++--- grafana-plugin/src/style/vars.css | 23 ++++++++++++++++++- 24 files changed, 110 insertions(+), 63 deletions(-) diff --git a/grafana-plugin/src/components/Modal/Modal.module.css b/grafana-plugin/src/components/Modal/Modal.module.css index a117b44d..683a78ad 100644 --- a/grafana-plugin/src/components/Modal/Modal.module.css +++ b/grafana-plugin/src/components/Modal/Modal.module.css @@ -13,9 +13,9 @@ border-image: initial; outline: none; padding: 15px; - background: #181b1f; - border: 1px solid #2d2e35; - box-shadow: 0 2px 4px 2px rgba(10, 10, 16, 0.1), 0 8px 16px rgba(10, 10, 16, 0.2), 0 12px 24px rgba(3, 3, 8, 0.3), 0 16px 32px rgba(3, 3, 8, 0.8); + background: var(--background-primary); + border: var(--border-weak); + box-shadow: var(--shadows-z3); border-radius: 2px; } diff --git a/grafana-plugin/src/components/Table/Table.module.css b/grafana-plugin/src/components/Table/Table.module.css index df6caa08..50e50ffc 100644 --- a/grafana-plugin/src/components/Table/Table.module.css +++ b/grafana-plugin/src/components/Table/Table.module.css @@ -4,17 +4,14 @@ .root table { width: 100%; - background: #22252b; } .root tr { - border-bottom: 1px solid #181b1f; - height: 60px; + min-height: 56px; } .root tr:hover { /* background: var(--secondary-background); */ - background: rgba(63, 62, 62, 0.45); } .root th:first-child { @@ -33,6 +30,7 @@ .expand-icon { padding: 10px; + color: var(--primary-text-color); pointer-events: none; transform: rotate(-90deg); transform-origin: center; diff --git a/grafana-plugin/src/components/Table/Table.tsx b/grafana-plugin/src/components/Table/Table.tsx index d639ccf0..dbc5e652 100644 --- a/grafana-plugin/src/components/Table/Table.tsx +++ b/grafana-plugin/src/components/Table/Table.tsx @@ -51,7 +51,7 @@ const GTable: FC = (props) => { { clearBeforeEdit = false, hidden = false, editModalTitle = 'New value', + style, } = props; const [isEditMode, setIsEditMode] = useState(false); @@ -87,6 +88,7 @@ const Text: TextInterface = (props) => { 'no-wrap': !wrap, keyboard, })} + style={style} > {hidden ? PLACEHOLDER : children} {editable && ( @@ -148,12 +150,12 @@ interface TitleProps extends TextProps { } const Title: FC = (props) => { - const { level, className, ...restProps } = props; + const { level, className, style, ...restProps } = props; // @ts-ignore const Tag: keyof JSX.IntrinsicElements = `h${level}`; return ( - + ); diff --git a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.module.css b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.module.css index e4cb9d63..ccae5a02 100644 --- a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.module.css +++ b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.module.css @@ -8,7 +8,6 @@ font-weight: 400; font-size: 12px; line-height: 16px; - color: rgba(204, 204, 220, 0.65); pointer-events: none; } diff --git a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx index 91cac6d6..1ae46c1e 100644 --- a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx +++ b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx @@ -3,6 +3,8 @@ import React, { FC, useMemo } from 'react'; import cn from 'classnames/bind'; import dayjs from 'dayjs'; +import Text from 'components/Text/Text'; + import styles from './TimelineMarks.module.css'; interface TimelineMarksProps { @@ -60,7 +62,9 @@ const TimelineMarks: FC = (props) => { {momentsToRender.map((m, i) => { return (
-
{m.moment.format('ddd D MMM')}
+
+ {m.moment.format('ddd D MMM')} +
{m.moments.map((mm, j) => (
@@ -69,7 +73,7 @@ const TimelineMarks: FC = (props) => { 'weekday-time-title__hidden': i === 0 && j === 0, })} > - {mm.format('HH:mm')} + {mm.format('HH:mm')}
))} diff --git a/grafana-plugin/src/components/UserGroups/UserGroups.module.css b/grafana-plugin/src/components/UserGroups/UserGroups.module.css index 178e3412..d48834b8 100644 --- a/grafana-plugin/src/components/UserGroups/UserGroups.module.css +++ b/grafana-plugin/src/components/UserGroups/UserGroups.module.css @@ -13,7 +13,6 @@ font-size: 12px; line-height: 16px; text-align: center; - color: rgba(204, 204, 220, 0.4); margin: 4px 0; display: flex; align-items: center; @@ -27,7 +26,7 @@ display: block; content: ""; flex-grow: 1; - border-bottom: 1px solid rgba(204, 204, 220, 0.15); + border-bottom: var(--border-medium); height: 0; margin-right: 5px; } @@ -36,7 +35,7 @@ display: block; content: ""; flex-grow: 1; - border-bottom: 1px solid rgba(204, 204, 220, 0.15); + border-bottom: var(--border-medium); height: 0; margin-left: 5px; } @@ -69,9 +68,13 @@ background: var(--hover-selected-hardcoded); } -.delete-icon { - /* display: none; */ +.icon { display: block; + color: var(--always-gray); +} + +.icon:hover { + color: white; } .user:hover .delete-icon { diff --git a/grafana-plugin/src/components/UserGroups/UserGroups.tsx b/grafana-plugin/src/components/UserGroups/UserGroups.tsx index affdad35..e7953645 100644 --- a/grafana-plugin/src/components/UserGroups/UserGroups.tsx +++ b/grafana-plugin/src/components/UserGroups/UserGroups.tsx @@ -5,6 +5,7 @@ import { arrayMoveImmutable } from 'array-move'; import cn from 'classnames/bind'; import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc'; +import Text from 'components/Text/Text'; import RemoteSelect from 'containers/RemoteSelect/RemoteSelect'; import { User } from 'models/user/user.types'; @@ -23,7 +24,7 @@ interface UserGroupsProps { const cx = cn.bind(styles); -const DragHandle = () => ; +const DragHandle = () => ; const SortableHandleHoc = SortableHandle(DragHandle); @@ -94,7 +95,7 @@ const UserGroups = (props: UserGroupsProps) => { {renderUser(item.data)}
- +
@@ -155,14 +156,16 @@ const SortableList = SortableContainer(({ items, handleAddGro ) : isMultipleGroups ? ( -
  • {item.data.name}
  • +
  • + {item.data.name} +
  • ) : null )} {isMultipleGroups && items[items.length - 1]?.type === 'item' && (
  • - Add user group + + Add user group +
  • )} diff --git a/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx b/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx index aa3fabf7..83a4882f 100644 --- a/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx +++ b/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx @@ -90,7 +90,7 @@ const UserTimezoneSelect: FC = (props) => { return (
    -
    ); }; diff --git a/grafana-plugin/src/containers/Rotation/Rotation.module.css b/grafana-plugin/src/containers/Rotation/Rotation.module.css index b721e17b..4fbcd226 100644 --- a/grafana-plugin/src/containers/Rotation/Rotation.module.css +++ b/grafana-plugin/src/containers/Rotation/Rotation.module.css @@ -54,7 +54,7 @@ position: absolute; left: 450px; width: 1px; - background: #fff; + background: var(--gradient-brandVertical); top: -10px; bottom: -10px; z-index: 1; diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx index a7a2b406..ffc2ada9 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx @@ -133,7 +133,7 @@ const RotationForm: FC = observer((props) => { return ( <>
    - {name} ({desc}) + {name} ({desc})
    = (props) => { return ( <>
    - {name} ({desc}) + {name} ({desc})
    {
    -
    Rotations
    +
    + + Rotations + +
    @@ -109,7 +114,7 @@ class Rotations extends Component {
    - Layer {layer.priority} + Layer {layer.priority} {/**/}
    @@ -151,8 +156,7 @@ class Rotations extends Component {
    - Layer 1 - {/* */} + Layer 1
    @@ -182,7 +186,7 @@ class Rotations extends Component { this.handleAddLayer(nextPriority); }} > - + Add rotations layer + + Add rotations layer
    )}
    diff --git a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx index af91a512..31192ffc 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx @@ -6,6 +6,7 @@ import dayjs from 'dayjs'; import { observer } from 'mobx-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import Text from 'components/Text/Text'; import TimelineMarks from 'components/TimelineMarks/TimelineMarks'; import Rotation from 'containers/Rotation/Rotation'; import { getColor, getFromString, getOverrideColor } from 'models/schedule/schedule.helpers'; @@ -73,7 +74,11 @@ class ScheduleFinal extends Component -
    Final schedule
    +
    + + Final schedule + +
    {/*} placeholder="Search..." diff --git a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx index 253c4c46..f6ba5f3b 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx @@ -6,6 +6,7 @@ import dayjs from 'dayjs'; import { observer } from 'mobx-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import Text from 'components/Text/Text'; import TimelineMarks from 'components/TimelineMarks/TimelineMarks'; import Rotation from 'containers/Rotation/Rotation'; import { RotationCreateData } from 'containers/RotationForm/RotationForm.types'; @@ -69,7 +70,11 @@ class ScheduleOverrides extends Component
    -
    Overrides
    +
    + + Overrides + +
    diff --git a/grafana-plugin/src/containers/ScheduleSlot/ScheduleSlot.tsx b/grafana-plugin/src/containers/ScheduleSlot/ScheduleSlot.tsx index 406bef15..374b0e3c 100644 --- a/grafana-plugin/src/containers/ScheduleSlot/ScheduleSlot.tsx +++ b/grafana-plugin/src/containers/ScheduleSlot/ScheduleSlot.tsx @@ -166,8 +166,8 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => { - {dayjs(event.start).tz(user.timezone).format('DD MMM, HH:mm')} - {dayjs(event.end).tz(user.timezone).format('DD MMM, HH:mm')} + {dayjs(event.start).tz(user?.timezone).format('DD MMM, HH:mm')} + {dayjs(event.end).tz(user?.timezone).format('DD MMM, HH:mm')} diff --git a/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.module.css b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.module.css index 0eab0c54..94166b47 100644 --- a/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.module.css +++ b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.module.css @@ -2,8 +2,8 @@ border: var(--border-medium); display: flex; flex-direction: column; - border-radius: 2px; - background: var(--primary-background); + background: var(--background-secondary); + border-radius: var(--border-radius); } .header { @@ -11,7 +11,7 @@ } .title { - font-weight: 500; + font-weight: 400; font-size: 19px; line-height: 24px; color: rgba(204, 204, 220, 0.65); @@ -22,7 +22,7 @@ position: absolute; left: 0; width: 1px; - background: #fff; + background: var(--gradient-brandVertical); top: 0; bottom: 0; z-index: 0; @@ -70,6 +70,7 @@ font-size: 12px; line-height: 16px; background: #454952; + color: #ccccdc; border-radius: 8px; text-align: center; transition: opacity 200ms ease, left 200ms ease; @@ -114,7 +115,6 @@ top: -24px; display: flex; font-weight: 400; - font-size: 14px; line-height: 20px; color: rgba(204, 204, 220, 0.65); width: 100%; diff --git a/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx index 497d4fd4..59b6edaa 100644 --- a/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx +++ b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx @@ -71,7 +71,11 @@ const UsersTimezones: FC = (props) => {
    -
    Schedule team and timezones
    +
    + + Schedule team and timezones + +
    {/* Current schedule users only @@ -98,12 +102,18 @@ const UsersTimezones: FC = (props) => { 'time-mark-text__translated': index > 0, })} > - {mm.format('HH:mm')} + + {mm.format('HH:mm')} +
    ))}
    - 24:00 + + + 24:00 + +
    diff --git a/grafana-plugin/src/icons/index.tsx b/grafana-plugin/src/icons/index.tsx index ffc6f75c..ff0afb08 100644 --- a/grafana-plugin/src/icons/index.tsx +++ b/grafana-plugin/src/icons/index.tsx @@ -238,7 +238,7 @@ export const ExpandIcon = (props: IconProps) => { diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index 0570ee98..0079d795 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -93,7 +93,7 @@ export class UserStore extends BaseStore { this.items = { ...this.items, - [user.pk]: user, + [user.pk]: { ...user, timezone: getTimezone(user) }, }; } diff --git a/grafana-plugin/src/pages/schedule/Schedule.module.css b/grafana-plugin/src/pages/schedule/Schedule.module.css index 2e5746bd..09d0aa98 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.module.css +++ b/grafana-plugin/src/pages/schedule/Schedule.module.css @@ -5,7 +5,7 @@ margin-top: 24px; --rotations-border: var(--border-medium); - --rotations-background: var(--primary-background); + --rotations-background: var(--background-secondary); } .header { diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index bff532ae..8e05ea45 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -116,7 +116,7 @@ class SchedulePage extends React.Component - + {schedule?.name} {/* -
    + {startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')} -
    +
    {/* Date: Fri, 30 Sep 2022 12:25:51 -0300 Subject: [PATCH 60/89] Update slack lookup days when checking for next shift --- .../tasks/notify_ical_schedule_shift.py | 2 +- .../tests/test_notify_ical_schedule_shift.py | 85 ++++++++++++++----- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/engine/apps/alerts/tasks/notify_ical_schedule_shift.py b/engine/apps/alerts/tasks/notify_ical_schedule_shift.py index 1a261880..a23f2a4b 100644 --- a/engine/apps/alerts/tasks/notify_ical_schedule_shift.py +++ b/engine/apps/alerts/tasks/notify_ical_schedule_shift.py @@ -308,7 +308,7 @@ def notify_ical_schedule_shift(schedule_pk): new_shifts = sorted(new_shifts, key=lambda shift: shift["start"]) if len(new_shifts) != 0: - days_to_lookup = (new_shifts[-1]["end"].date() - now.date()).days + days_to_lookup = (new_shifts[-1]["end"].date() - now.date()).days + 1 days_to_lookup = max([days_to_lookup, MIN_DAYS_TO_LOOKUP_FOR_THE_END_OF_EVENT]) else: days_to_lookup = MIN_DAYS_TO_LOOKUP_FOR_THE_END_OF_EVENT diff --git a/engine/apps/alerts/tests/test_notify_ical_schedule_shift.py b/engine/apps/alerts/tests/test_notify_ical_schedule_shift.py index d6cb6398..eae19ef3 100644 --- a/engine/apps/alerts/tests/test_notify_ical_schedule_shift.py +++ b/engine/apps/alerts/tests/test_notify_ical_schedule_shift.py @@ -1,4 +1,9 @@ +from datetime import datetime +from unittest.mock import Mock, patch + import pytest +import pytz +from django.utils import timezone from apps.alerts.tasks.notify_ical_schedule_shift import notify_ical_schedule_shift from apps.schedules.models import OnCallScheduleICal @@ -9,32 +14,35 @@ PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH -X-WR-CALNAME:t -X-WR-TIMEZONE:Asia/Yekaterinburg -BEGIN:VTIMEZONE -TZID:Asia/Yekaterinburg -X-LIC-LOCATION:Asia/Yekaterinburg -BEGIN:STANDARD -TZOFFSETFROM:+0500 -TZOFFSETTO:+0500 -TZNAME:+05 -DTSTART:19700101T000000 -END:STANDARD -END:VTIMEZONE BEGIN:VEVENT -DTSTART;TZID=Asia/Yekaterinburg:20210124T130000 -DTEND;TZID=Asia/Yekaterinburg:20210124T220000 -RRULE:FREQ=DAILY -DTSTAMP:20210127T143634Z -UID:0i0af8p6p8vfampe3r1vkog0jg@google.com -CREATED:20210127T143553Z +DTSTART;VALUE=DATE:20211005 +DTEND;VALUE=DATE:20211012 +RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=7;BYDAY=WE +DTSTAMP:20210930T125523Z +UID:id1@google.com +CREATED:20210928T202349Z DESCRIPTION: -LAST-MODIFIED:20210127T143553Z +LAST-MODIFIED:20210929T204751Z LOCATION: -SEQUENCE:0 +SEQUENCE:1 STATUS:CONFIRMED -SUMMARY:@Bernard Desruisseaux -TRANSP:OPAQUE +SUMMARY:user1 +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20210928 +DTEND;VALUE=DATE:20211005 +RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=7;BYDAY=WE +DTSTAMP:20210930T125523Z +UID:id2@google.com +CREATED:20210928T202331Z +DESCRIPTION: +LAST-MODIFIED:20210929T204744Z +LOCATION: +SEQUENCE:2 +STATUS:CONFIRMED +SUMMARY:user2 +TRANSP:TRANSPARENT END:VEVENT END:VCALENDAR """ @@ -61,3 +69,36 @@ def test_current_overrides_ical_schedule_is_none( # this should not raise notify_ical_schedule_shift(ical_schedule.oncallschedule_ptr_id) + + +@pytest.mark.django_db +def test_next_shift_notification_long_shifts( + make_organization_and_user_with_slack_identities, + make_schedule, + make_user, +): + organization, _, _, _ = make_organization_and_user_with_slack_identities() + make_user(organization=organization, username="user1") + make_user(organization=organization, username="user2") + + ical_schedule = make_schedule( + organization, + schedule_class=OnCallScheduleICal, + name="test_ical_schedule", + channel="channel", + ical_url_primary="url", + prev_ical_file_primary=ICAL_DATA, + cached_ical_file_primary=ICAL_DATA, + prev_ical_file_overrides=None, + cached_ical_file_overrides=None, + ) + + with patch.object(timezone, "datetime", Mock(wraps=timezone.datetime)) as mock_tz_datetime: + mock_tz_datetime.now.return_value = datetime(2021, 9, 29, 12, 0, tzinfo=pytz.UTC) + with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + notify_ical_schedule_shift(ical_schedule.oncallschedule_ptr_id) + + slack_blocks = mock_slack_api_call.call_args_list[0][1]["blocks"] + notification = slack_blocks[0]["text"]["text"] + assert "*New on-call shift:*\nuser2" in notification + assert "*Next on-call shift:*\nuser1" in notification From 7d9d23945201f8869829e9819b72eca8cea453c3 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Fri, 30 Sep 2022 10:14:39 -0600 Subject: [PATCH 61/89] Catch exception for when oncall user is out of sync with slack user --- .../apps/slack/scenarios/resolution_note.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/engine/apps/slack/scenarios/resolution_note.py b/engine/apps/slack/scenarios/resolution_note.py index cec692c6..bf328548 100644 --- a/engine/apps/slack/scenarios/resolution_note.py +++ b/engine/apps/slack/scenarios/resolution_note.py @@ -7,6 +7,7 @@ from django.utils import timezone from apps.slack.scenarios import scenario_step from apps.slack.slack_client.exceptions import SlackAPIException +from apps.user_management.models import User from common.api_helpers.utils import create_engine_url from .step_mixins import CheckAlertIsUnarchivedMixin @@ -107,10 +108,19 @@ class AddToResolutionNoteStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari channel_id=channel_id, ) alert_group = slack_message.get_alert_group() - author_slack_user_identity = SlackUserIdentity.objects.get( - slack_id=payload["message"]["user"], slack_team_identity=slack_team_identity - ) - author_user = self.organization.users.get(slack_user_identity=author_slack_user_identity) + try: + author_slack_user_identity = SlackUserIdentity.objects.get( + slack_id=payload["message"]["user"], slack_team_identity=slack_team_identity + ) + author_user = self.organization.users.get(slack_user_identity=author_slack_user_identity) + except (SlackUserIdentity.DoesNotExist, User.DoesNotExist): + warning_text = ( + "Unable to add this message to resolution note: could not find corresponding" + "OnCall user for message author: {}".format(payload["message"]["user"]) + ) + self.open_warning_window(payload, warning_text) + return + resolution_note_slack_message = ResolutionNoteSlackMessage( alert_group=alert_group, user=author_user, @@ -121,6 +131,7 @@ class AddToResolutionNoteStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari ts=message_ts, permalink=permalink, ) + resolution_note_slack_message.added_to_resolution_note = True resolution_note_slack_message.save() resolution_note = resolution_note_slack_message.get_resolution_note() From 6dde87afded03914f83bba9b8a436b8a5c6b1147 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Fri, 30 Sep 2022 10:16:03 -0600 Subject: [PATCH 62/89] Message format --- engine/apps/slack/scenarios/resolution_note.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/apps/slack/scenarios/resolution_note.py b/engine/apps/slack/scenarios/resolution_note.py index bf328548..e989a9d6 100644 --- a/engine/apps/slack/scenarios/resolution_note.py +++ b/engine/apps/slack/scenarios/resolution_note.py @@ -115,7 +115,7 @@ class AddToResolutionNoteStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari author_user = self.organization.users.get(slack_user_identity=author_slack_user_identity) except (SlackUserIdentity.DoesNotExist, User.DoesNotExist): warning_text = ( - "Unable to add this message to resolution note: could not find corresponding" + "Unable to add this message to resolution note: could not find corresponding " "OnCall user for message author: {}".format(payload["message"]["user"]) ) self.open_warning_window(payload, warning_text) From a1c3ba330d5982597703785c3b5f1d5847d724b4 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Fri, 30 Sep 2022 10:26:47 -0600 Subject: [PATCH 63/89] Format --- engine/apps/slack/scenarios/resolution_note.py | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/apps/slack/scenarios/resolution_note.py b/engine/apps/slack/scenarios/resolution_note.py index e989a9d6..c22a14ad 100644 --- a/engine/apps/slack/scenarios/resolution_note.py +++ b/engine/apps/slack/scenarios/resolution_note.py @@ -120,7 +120,6 @@ class AddToResolutionNoteStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari ) self.open_warning_window(payload, warning_text) return - resolution_note_slack_message = ResolutionNoteSlackMessage( alert_group=alert_group, user=author_user, From 2bfb834e92f4e7eb02760bcda33daa3f1fff67f3 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Fri, 30 Sep 2022 12:40:22 -0600 Subject: [PATCH 64/89] Update changelog for v1.0.38 --- CHANGELOG.md | 5 +++- grafana-plugin/CHANGELOG.md | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1cdd055..d0ead17d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # Change Log -## v1.0.38 (2022-09-22) +## v1.0.38 (2022-09-30) +- Fix exception handling for adding resolution notes when slack and oncall users are out of sync. +- Fix all day events showing as having gaps in slack notifications +- Improve plugin configuration error message readability - Add `telegram` key to `permalinks` property in `AlertGroup` public API response schema ## v1.0.37 (2022-09-21) diff --git a/grafana-plugin/CHANGELOG.md b/grafana-plugin/CHANGELOG.md index ffaeb731..d0ead17d 100644 --- a/grafana-plugin/CHANGELOG.md +++ b/grafana-plugin/CHANGELOG.md @@ -1,67 +1,106 @@ # Change Log +## v1.0.38 (2022-09-30) + +- Fix exception handling for adding resolution notes when slack and oncall users are out of sync. +- Fix all day events showing as having gaps in slack notifications +- Improve plugin configuration error message readability +- Add `telegram` key to `permalinks` property in `AlertGroup` public API response schema + +## v1.0.37 (2022-09-21) + +- Improve API token creation form +- Fix alert group bulk action bugs +- Add `permalinks` property to `AlertGroup` public API response schema +- Scheduling system bug fixes +- Public API bug fixes + +## v1.0.36 (2022-09-12) + +- Alpha web schedules frontend/backend updates +- Bug fixes + ## v1.0.35 (2022-09-07) + - Bug fixes ## v1.0.34 (2022-09-06) + - Fix schedule notification spam ## v1.0.33 (2022-09-06) + - Add raw alert view - Add GitHub star button for OSS installations - Restore alert group search functionality - Bug fixes ## v1.0.32 (2022-09-01) + - Bug fixes ## v1.0.31 (2022-09-01) + - Bump celery version - Fix oss to cloud connection ## v1.0.30 (2022-08-31) + - Bug fix: check user notification policy before access ## v1.0.29 (2022-08-31) + - Add arm64 docker image ## v1.0.28 (2022-08-31) + - Bug fixes ## v1.0.27 (2022-08-30) + - Bug fixes ## v1.0.26 (2022-08-26) + - Insight log's format fixes - Remove UserNotificationPolicy auto-recreating ## v1.0.25 (2022-08-24) + - Bug fixes ## v1.0.24 (2022-08-24) + - Insight logs - Default DATA_UPLOAD_MAX_MEMORY_SIZE to 1mb ## v1.0.23 (2022-08-23) + - Bug fixes ## v1.0.22 (2022-08-16) + - Make STATIC_URL configurable from environment variable ## v1.0.21 (2022-08-12) + - Bug fixes ## v1.0.19 (2022-08-10) + - Bug fixes ## v1.0.15 (2022-08-03) + - Bug fixes ## v1.0.13 (2022-07-27) + - Optimize alert group list view - Fix a bug related to Twilio setup ## v1.0.12 (2022-07-26) + - Update push-notifications dependency - Rework how absolute URLs are built - Fix to show maintenance windows per team @@ -69,15 +108,18 @@ - Internal api to get a schedule final events ## v1.0.10 (2022-07-22) + - Speed-up of alert group web caching - Internal api for OnCall shifts ## v1.0.9 (2022-07-21) + - Frontend bug fixes & improvements - Support regex_replace() in templates - Bring back alert group caching and list view ## v1.0.7 (2022-07-18) + - Backend & frontend bug fixes - Deployment improvements - Reshape webhook payload for outgoing webhooks @@ -85,18 +127,22 @@ - Improve alert group list load speeds and simplify caching system ## v1.0.6 (2022-07-12) + - Manual Incidents enabled for teams - Fix phone notifications for OSS - Public API improvements ## v1.0.5 (2022-07-06) + - Bump Django to 3.2.14 - Fix PagerDuty iCal parsing ## 1.0.4 (2022-06-28) + - Allow Telegram DMs without channel connection. ## 1.0.3 (2022-06-27) + - Fix users public api endpoint. Now it returns users with all roles. - Fix redundant notifications about gaps in schedules. - Frontend fixes. From 2355fa3a3ce37380f56a33266ae0c1841feba73b Mon Sep 17 00:00:00 2001 From: toro_ponz Date: Sun, 2 Oct 2022 00:45:01 +0900 Subject: [PATCH 65/89] fix failure of "Format Alert" button on Slack. --- engine/apps/slack/scenarios/alertgroup_appearance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/apps/slack/scenarios/alertgroup_appearance.py b/engine/apps/slack/scenarios/alertgroup_appearance.py index 7b772a0e..c4c4236b 100644 --- a/engine/apps/slack/scenarios/alertgroup_appearance.py +++ b/engine/apps/slack/scenarios/alertgroup_appearance.py @@ -57,7 +57,8 @@ class OpenAlertAppearanceDialogStep( # This is a special case for amazon sns notifications in str format CHEKED if ( - AlertReceiveChannel.INTEGRATION_AMAZON_SNS is not None + hasattr(AlertReceiveChannel, "INTEGRATION_AMAZON_SNS") + and AlertReceiveChannel.INTEGRATION_AMAZON_SNS is not None and alert_group.channel.integration == AlertReceiveChannel.INTEGRATION_AMAZON_SNS and raw_request_data == "{}" ): From a8a88f5c00b763753c6392d7b5e26494d47776ae Mon Sep 17 00:00:00 2001 From: toro_ponz Date: Mon, 3 Oct 2022 18:54:56 +0900 Subject: [PATCH 66/89] remove wasted condition check. --- engine/apps/slack/scenarios/alertgroup_appearance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/apps/slack/scenarios/alertgroup_appearance.py b/engine/apps/slack/scenarios/alertgroup_appearance.py index c4c4236b..8a335fcd 100644 --- a/engine/apps/slack/scenarios/alertgroup_appearance.py +++ b/engine/apps/slack/scenarios/alertgroup_appearance.py @@ -58,7 +58,6 @@ class OpenAlertAppearanceDialogStep( # This is a special case for amazon sns notifications in str format CHEKED if ( hasattr(AlertReceiveChannel, "INTEGRATION_AMAZON_SNS") - and AlertReceiveChannel.INTEGRATION_AMAZON_SNS is not None and alert_group.channel.integration == AlertReceiveChannel.INTEGRATION_AMAZON_SNS and raw_request_data == "{}" ): From 5e272f85657009123650be6c76861a6712a956f3 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 3 Oct 2022 11:22:02 +0100 Subject: [PATCH 67/89] Fix failing tests due to bug in month calculations (#599) --- .../schedules/models/custom_on_call_shift.py | 11 ++++++- .../tests/test_custom_on_call_shift.py | 32 +++++++++---------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 232c824a..7b24676a 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -5,6 +5,7 @@ from calendar import monthrange from uuid import uuid4 import pytz +from dateutil import relativedelta from django.apps import apps from django.conf import settings from django.core.validators import MinLengthValidator @@ -353,6 +354,13 @@ class CustomOnCallShift(models.Model): ONE_DAY = 1 ONE_HOUR = 1 + def add_months(year, month, months_add): + """ + Utility method for month calculation. E.g. (2022, 12) + 1 month = (2023, 1) + """ + dt = timezone.datetime.min.replace(year=year, month=month) + relativedelta.relativedelta(months=months_add) + return dt.year, dt.month + current_event = Event.from_ical(event_ical) # take shift interval, not event interval. For rolling_users shift it is not the same. interval = self.interval or 1 @@ -385,7 +393,8 @@ class CustomOnCallShift(models.Model): days_for_next_event = DAYS_IN_A_MONTH - current_event_start.day + ONE_DAY # count next event start date with respect to event interval for i in range(1, interval): - next_month_days = monthrange(current_event_start.year, current_event_start.month + i)[1] + year, month = add_months(current_event_start.year, current_event_start.month, i) + next_month_days = monthrange(year, month)[1] days_for_next_event += next_month_days next_event_start = current_event_start + timezone.timedelta(days=days_for_next_event) diff --git a/engine/apps/schedules/tests/test_custom_on_call_shift.py b/engine/apps/schedules/tests/test_custom_on_call_shift.py index dd3cfba6..4dbf6029 100644 --- a/engine/apps/schedules/tests/test_custom_on_call_shift.py +++ b/engine/apps/schedules/tests/test_custom_on_call_shift.py @@ -354,11 +354,11 @@ def test_rolling_users_event_with_interval_monthly( user_2 = make_user_for_organization(organization) schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) - start_date = timezone.now().replace(day=1, microsecond=0) - days_for_next_month_1 = monthrange(start_date.year, start_date.month)[1] - days_for_next_month_2 = monthrange(start_date.year, start_date.month + 1)[1] + days_for_next_month_1 - days_for_next_month_3 = monthrange(start_date.year, start_date.month + 2)[1] + days_for_next_month_2 - days_for_next_month_4 = monthrange(start_date.year, start_date.month + 3)[1] + days_for_next_month_3 + start_date = timezone.datetime(year=2022, month=10, day=1, hour=10, minute=30) + days_for_next_month_1 = monthrange(2022, 10)[1] + days_for_next_month_2 = monthrange(2022, 11)[1] + days_for_next_month_1 + days_for_next_month_3 = monthrange(2022, 12)[1] + days_for_next_month_2 + days_for_next_month_4 = monthrange(2023, 1)[1] + days_for_next_month_3 data = { "priority_level": 1, @@ -718,19 +718,19 @@ def test_rolling_users_with_diff_start_and_rotation_start_monthly( user_3 = make_user_for_organization(organization) schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) - now = timezone.now().replace(day=1, microsecond=0) - days_in_curr_month = monthrange(now.year, now.month)[1] - days_in_next_month = monthrange(now.year, now.month + 1)[1] + start_date = timezone.datetime(year=2022, month=12, day=1, hour=10, minute=30) + days_in_curr_month = monthrange(2022, 12)[1] + days_in_next_month = monthrange(2023, 1)[1] data = { "priority_level": 1, - "start": now, - "week_start": now.weekday(), - "rotation_start": now + timezone.timedelta(days=days_in_curr_month - 1, hours=1), + "start": start_date, + "week_start": start_date.weekday(), + "rotation_start": start_date + timezone.timedelta(days=days_in_curr_month - 1, hours=1), "duration": timezone.timedelta(seconds=1800), "frequency": CustomOnCallShift.FREQUENCY_MONTHLY, "schedule": schedule, - "until": now + timezone.timedelta(days=days_in_curr_month + days_in_next_month + 10, minutes=1), + "until": start_date + timezone.timedelta(days=days_in_curr_month + days_in_next_month + 10, minutes=1), } rolling_users = [[user_1], [user_2], [user_3]] on_call_shift = make_on_call_shift( @@ -738,7 +738,7 @@ def test_rolling_users_with_diff_start_and_rotation_start_monthly( ) on_call_shift.add_rolling_users(rolling_users) - date = now + timezone.timedelta(minutes=5) + date = start_date + timezone.timedelta(minutes=5) # rotation starts from user_2, because user_1 started earlier than rotation start date user_2_on_call_dates = [date + timezone.timedelta(days=days_in_curr_month)] user_3_on_call_dates = [date + timezone.timedelta(days=days_in_curr_month + days_in_next_month)] @@ -774,9 +774,9 @@ def test_rolling_users_with_diff_start_and_rotation_start_monthly_by_monthday( user_3 = make_user_for_organization(organization) schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) - start_date = timezone.now().replace(day=1, microsecond=0) - days_in_curr_month = monthrange(start_date.year, start_date.month)[1] - days_in_next_month = monthrange(start_date.year, start_date.month + 1)[1] + start_date = timezone.datetime(year=2022, month=12, day=1, hour=10, minute=30) + days_in_curr_month = monthrange(2022, 12)[1] + days_in_next_month = monthrange(2023, 1)[1] data = { "priority_level": 1, From f8314ef9c2852ee0326930a5edae65a6094034d3 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Mon, 3 Oct 2022 07:37:59 -0300 Subject: [PATCH 68/89] Fix timing issue with schedule tests reusing cached users (#592) Co-authored-by: Vadim Stepanov --- engine/apps/api/tests/test_schedules.py | 17 +++++++++++------ .../schedules/tests/test_on_call_schedule.py | 3 +++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 7139f2e9..19537391 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -10,6 +10,7 @@ from rest_framework.serializers import ValidationError from rest_framework.test import APIClient from apps.alerts.models import EscalationPolicy +from apps.schedules.ical_utils import memoized_users_in_ical from apps.schedules.models import ( CustomOnCallShift, OnCallSchedule, @@ -742,6 +743,8 @@ def test_filter_events_final_schedule( request_date = start_date user_a, user_b, user_c, user_d, user_e = (make_user_for_organization(organization, username=i) for i in "ABCDE") + # clear users pks <-> organization cache (persisting between tests) + memoized_users_in_ical.cache_clear() shifts = ( # user, priority, start time (h), duration (hs) @@ -837,7 +840,7 @@ def test_next_shifts_per_user( make_schedule, make_on_call_shift, ): - organization, user, token = make_organization_and_user_with_plugin_token() + organization, admin, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( @@ -848,6 +851,8 @@ def test_next_shifts_per_user( tomorrow = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + timezone.timedelta(days=1) user_a, user_b, user_c, user_d = (make_user_for_organization(organization, username=i) for i in "ABCD") + # clear users pks <-> organization cache (persisting between tests) + memoized_users_in_ical.cache_clear() shifts = ( # user, priority, start time (h), duration (hs) @@ -860,16 +865,16 @@ def test_next_shifts_per_user( for user, priority, start_h, duration in shifts: data = { "start": tomorrow + timezone.timedelta(hours=start_h), - "rotation_start": tomorrow, + "rotation_start": tomorrow + timezone.timedelta(hours=start_h), "duration": timezone.timedelta(hours=duration), "priority_level": priority, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "schedule": schedule, } on_call_shift = make_on_call_shift( - organization=organization, shift_type=CustomOnCallShift.TYPE_RECURRENT_EVENT, **data + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data ) - on_call_shift.users.add(user) + on_call_shift.add_rolling_users([[user]]) # override in the past: 17-18 / D # won't be listed, but user D will still be included in the response @@ -896,10 +901,10 @@ def test_next_shifts_per_user( ) override.add_rolling_users([[user_c]]) - # final sdhedule: 7-12: B, 15-16: A, 16-17: B, 17-18: C (override), 18-20: C + # final schedule: 7-12: B, 15-16: A, 16-17: B, 17-18: C (override), 18-20: C url = reverse("api-internal:schedule-next-shifts-per-user", kwargs={"pk": schedule.public_primary_key}) - response = client.get(url, format="json", **make_user_auth_headers(user, token)) + response = client.get(url, format="json", **make_user_auth_headers(admin, token)) assert response.status_code == status.HTTP_200_OK expected = { diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index e8da0e67..bb9699e7 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -4,6 +4,7 @@ import pytest import pytz from django.utils import timezone +from apps.schedules.ical_utils import memoized_users_in_ical from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleCalendar, OnCallScheduleWeb from common.constants.role import Role @@ -236,6 +237,8 @@ def test_filter_events_ical_all_day(make_organization, make_user_for_organizatio schedule.cached_ical_file_primary = calendar.to_ical() for u in ("@Bernard Desruisseaux", "@Bob", "@Alex"): make_user_for_organization(organization, username=u) + # clear users pks <-> organization cache (persisting between tests) + memoized_users_in_ical.cache_clear() day_to_check_iso = "2021-01-27T15:27:14.448059+00:00" parsed_iso_day_to_check = datetime.datetime.fromisoformat(day_to_check_iso).replace(tzinfo=pytz.UTC) From dd55c5a9f9bbb70b0b21fe58d213d49c3f8bc6d4 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 3 Oct 2022 15:14:14 +0300 Subject: [PATCH 69/89] schedules fix --- .../src/pages/schedules/Schedules.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 230d2c84..cb7b1655 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -85,7 +85,13 @@ class SchedulesPage extends React.Component this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } })); @@ -94,12 +100,13 @@ class SchedulesPage extends React.Component res.id === id)?.id; - if (scheduleId || id === 'new') { - this.setState({ scheduleIdToEdit: id }); - } else { - openErrorNotification(`Schedule with id=${id} is not found. Please select schedule from the list.`); - } + scheduleId = schedules && schedules.find((res) => res.id === id)?.id; + } + + if (scheduleId || isNewSchedule) { + this.setState({ scheduleIdToEdit: id }); + } else { + openErrorNotification(`Schedule with id=${id} is not found. Please select schedule from the list.`); } }; @@ -165,7 +172,7 @@ class SchedulesPage extends React.Component {() => ( <> From c7d02deaf898ed5b798f79be9ae598258ebb3e28 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 3 Oct 2022 15:19:24 +0300 Subject: [PATCH 70/89] remove unused linter --- grafana-plugin/src/pages/schedules/Schedules.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index cb7b1655..9493d837 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -85,7 +85,7 @@ class SchedulesPage extends React.Component {() => ( <> From 25341c2e2f3161eee68ab95a8025101cf5722b73 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 3 Oct 2022 17:03:26 +0300 Subject: [PATCH 71/89] polling changes --- .../src/pages/incidents/Incidents.tsx | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 34288a1a..e084c894 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -136,18 +136,15 @@ class Incidents extends React.Component }); } - const fetchIncidentData = () => { - store.alertGroupStore.updateIncidentFilters(filters, isOnMount); // this line fetches incidents - getLocationSrv().update({ query: { page: 'incidents', ...store.alertGroupStore.incidentFilters } }); - } + this.clearPollingInterval(); + this.setPollingInterval(filters, isOnMount); + this.fetchIncidentData(filters, isOnMount); + }; - if (this.pollingIntervalId) { - clearInterval(this.pollingIntervalId); - } - - this.pollingIntervalId = setInterval(() => fetchIncidentData(), POLLING_NUM_SECONDS * 1000); - - fetchIncidentData(); + fetchIncidentData = (filters: IncidentsFiltersType, isOnMount: boolean) => { + const { store } = this.props; + store.alertGroupStore.updateIncidentFilters(filters, isOnMount); // this line fetches incidents + getLocationSrv().update({ query: { page: 'incidents', ...store.alertGroupStore.incidentFilters } }); }; onChangeCursor = (cursor: string, direction: 'prev' | 'next') => { @@ -365,7 +362,7 @@ class Incidents extends React.Component } handleSelectedIncidentIdsChange = (ids: Array) => { - this.setState({ selectedIncidentIds: ids }); + this.setState({ selectedIncidentIds: ids }, () => ids?.length === 0 && this.setPollingInterval()); }; renderId(record: AlertType) { @@ -520,6 +517,8 @@ class Incidents extends React.Component }; getBulkActionClickHandler = (action: string | number) => { + this.clearPollingInterval(); + const { selectedIncidentIds, affectedRows } = this.state; const { store } = this.props; @@ -556,6 +555,15 @@ class Incidents extends React.Component store.alertGroupStore.updateIncidents(); }); }; + + clearPollingInterval() { + clearInterval(this.pollingIntervalId); + this.pollingIntervalId = undefined; + } + + setPollingInterval(filters: IncidentsFiltersType = this.state.filters, isOnMount: boolean = false) { + this.pollingIntervalId = setInterval(() => this.fetchIncidentData(filters, isOnMount), POLLING_NUM_SECONDS * 1000); + } } export default withMobXProviderContext(Incidents); From 6338e5878398715eda782aa6305bae75f13dd561 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 3 Oct 2022 17:25:13 +0300 Subject: [PATCH 72/89] fix for webhook linter --- .../pages/outgoing_webhooks/OutgoingWebhooks.tsx | 15 ++++++++++----- grafana-plugin/src/pages/schedules/Schedules.tsx | 5 ++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 8ddd846c..51d80e98 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -60,14 +60,19 @@ class OutgoingWebhooks extends React.Component this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } })); + } - if (outgoingWebhook) { - this.setState({ outgoingWebhookIdToEdit: id }); - } + if (outgoingWebhook || isNewWebhook) { + this.setState({ outgoingWebhookIdToEdit: id }); } }; diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 9493d837..3fb0d618 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -87,8 +87,8 @@ class SchedulesPage extends React.Component res.id === id)?.id; + scheduleId = schedule.id; } if (scheduleId || isNewSchedule) { From 0ee8f3e42b3833112f70dc5d0b80e5d5a30865f6 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Mon, 3 Oct 2022 11:18:49 -0600 Subject: [PATCH 73/89] Update changelog for v1.0.39 --- CHANGELOG.md | 4 ++++ grafana-plugin/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0ead17d..ebfa6fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## v1.0.39 (2022-10-03) + +- Fix issue in v1.0.38 blocking the creation of schedules and webhooks in the UI + ## v1.0.38 (2022-09-30) - Fix exception handling for adding resolution notes when slack and oncall users are out of sync. diff --git a/grafana-plugin/CHANGELOG.md b/grafana-plugin/CHANGELOG.md index d0ead17d..ebfa6fb2 100644 --- a/grafana-plugin/CHANGELOG.md +++ b/grafana-plugin/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## v1.0.39 (2022-10-03) + +- Fix issue in v1.0.38 blocking the creation of schedules and webhooks in the UI + ## v1.0.38 (2022-09-30) - Fix exception handling for adding resolution notes when slack and oncall users are out of sync. From f7c78038074ec03a4e6bb860dbecd5f96e3689b9 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Mon, 3 Oct 2022 14:27:22 -0300 Subject: [PATCH 74/89] Fix related_users for no-shifts schedule --- engine/apps/schedules/models/on_call_schedule.py | 1 + .../apps/schedules/tests/test_on_call_schedule.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/engine/apps/schedules/models/on_call_schedule.py b/engine/apps/schedules/models/on_call_schedule.py index b3d72911..24aa8d0d 100644 --- a/engine/apps/schedules/models/on_call_schedule.py +++ b/engine/apps/schedules/models/on_call_schedule.py @@ -654,6 +654,7 @@ class OnCallScheduleWeb(OnCallSchedule): for g in rolling_groups if g is not None ), + set(), ) return users diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index bb9699e7..a5380a7e 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -745,6 +745,19 @@ def test_preview_override_shift(make_organization, make_user_for_organization, m assert schedule._ical_file_overrides == schedule_overrides_ical +@pytest.mark.django_db +def test_schedule_related_users_empty_schedule(make_organization, make_schedule): + organization = make_organization() + schedule = make_schedule( + organization, + schedule_class=OnCallScheduleWeb, + name="test_web_schedule", + ) + + users = schedule.related_users() + assert users == set() + + @pytest.mark.django_db def test_schedule_related_users(make_organization, make_user_for_organization, make_on_call_shift, make_schedule): organization = make_organization() From cd7c77230e91a23b3b6c5357eb417624ff53f890 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 3 Oct 2022 20:42:30 +0300 Subject: [PATCH 75/89] improvements --- .../src/pages/incidents/Incidents.tsx | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index e084c894..d385a0e1 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -197,7 +197,11 @@ class Incidents extends React.Component {'resolve' in store.alertGroupStore.bulkActions && ( - @@ -207,7 +211,7 @@ class Incidents extends React.Component @@ -215,14 +219,21 @@ class Incidents extends React.Component )} {'silence' in store.alertGroupStore.bulkActions && ( - )} {'restart' in store.alertGroupStore.bulkActions && ( - + this.getBulkActionClickHandler('silence', ev)} + /> )} @@ -362,7 +373,9 @@ class Incidents extends React.Component } handleSelectedIncidentIdsChange = (ids: Array) => { - this.setState({ selectedIncidentIds: ids }, () => ids?.length === 0 && this.setPollingInterval()); + this.setState({ selectedIncidentIds: ids }, () => { + ids.length > 0 ? this.clearPollingInterval() : this.setPollingInterval(); + }); }; renderId(record: AlertType) { @@ -516,36 +529,34 @@ class Incidents extends React.Component }; }; - getBulkActionClickHandler = (action: string | number) => { - this.clearPollingInterval(); - + getBulkActionClickHandler = (action: string | number, event?: any) => { const { selectedIncidentIds, affectedRows } = this.state; const { store } = this.props; - return (event?: any) => { - store.alertGroupStore.liveUpdatesPaused = true; - const delay = typeof event === 'number' ? event : 0; + this.setPollingInterval(); - this.setState( - { - selectedIncidentIds: [], - affectedRows: selectedIncidentIds.reduce( - (acc, incidentId: AlertType['pk']) => ({ - ...acc, - [incidentId]: true, - }), - affectedRows - ), - }, - () => { - store.alertGroupStore.bulkAction({ - action, - alert_group_pks: selectedIncidentIds, - delay, - }); - } - ); - }; + store.alertGroupStore.liveUpdatesPaused = true; + const delay = typeof event === 'number' ? event : 0; + + this.setState( + { + selectedIncidentIds: [], + affectedRows: selectedIncidentIds.reduce( + (acc, incidentId: AlertType['pk']) => ({ + ...acc, + [incidentId]: true, + }), + affectedRows + ), + }, + () => { + store.alertGroupStore.bulkAction({ + action, + alert_group_pks: selectedIncidentIds, + delay, + }); + } + ); }; onIncidentsUpdateClick = () => { @@ -561,7 +572,7 @@ class Incidents extends React.Component this.pollingIntervalId = undefined; } - setPollingInterval(filters: IncidentsFiltersType = this.state.filters, isOnMount: boolean = false) { + setPollingInterval(filters: IncidentsFiltersType = this.state.filters, isOnMount = false) { this.pollingIntervalId = setInterval(() => this.fetchIncidentData(filters, isOnMount), POLLING_NUM_SECONDS * 1000); } } From 2a3fc397db988364a5494e65fd489fa6603ae03d Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Mon, 3 Oct 2022 15:00:23 -0300 Subject: [PATCH 76/89] Clear users cache before schedule tests logic/asserts --- engine/apps/api/tests/test_schedules.py | 2 ++ engine/apps/schedules/tests/test_on_call_schedule.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 19537391..13ef0825 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -985,6 +985,8 @@ def test_merging_same_shift_events( user_a = make_user_for_organization(organization) user_b = make_user_for_organization(organization) user_c = make_user_for_organization(organization, role=Role.VIEWER) + # clear users pks <-> organization cache (persisting between tests) + memoized_users_in_ical.cache_clear() data = { "start": start_date + timezone.timedelta(hours=10), diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index a5380a7e..8bb079f4 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -271,6 +271,8 @@ def test_final_schedule_events(make_organization, make_user_for_organization, ma start_date = now - timezone.timedelta(days=7) user_a, user_b, user_c, user_d, user_e = (make_user_for_organization(organization, username=i) for i in "ABCDE") + # clear users pks <-> organization cache (persisting between tests) + memoized_users_in_ical.cache_clear() shifts = ( # user, priority, start time (h), duration (hs) @@ -370,6 +372,8 @@ def test_final_schedule_splitting_events( start_date = now - timezone.timedelta(days=7) user_a, user_b, user_c = (make_user_for_organization(organization, username=i) for i in "ABC") + # clear users pks <-> organization cache (persisting between tests) + memoized_users_in_ical.cache_clear() shifts = ( # user, priority, start time (h), duration (hs) @@ -437,6 +441,8 @@ def test_final_schedule_splitting_same_time_events( start_date = now - timezone.timedelta(days=7) user_a, user_b, user_c = (make_user_for_organization(organization, username=i) for i in "ABC") + # clear users pks <-> organization cache (persisting between tests) + memoized_users_in_ical.cache_clear() shifts = ( # user, priority, start time (h), duration (hs) @@ -771,6 +777,8 @@ def test_schedule_related_users(make_organization, make_user_for_organization, m start_date = now - timezone.timedelta(days=7) user_a, _, _, user_d, user_e = (make_user_for_organization(organization, username=i) for i in "ABCDE") + # clear users pks <-> organization cache (persisting between tests) + memoized_users_in_ical.cache_clear() shifts = ( # user, priority, start time (h), duration (hs) From d8bf3d0bd0f4c766c22ca2fe5ffaad6f2fdf5d3b Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 4 Oct 2022 11:11:35 +0300 Subject: [PATCH 77/89] reused clearPollingInterval --- .../src/containers/IncidentsFilters/IncidentsFilters.tsx | 2 -- grafana-plugin/src/pages/incidents/Incidents.tsx | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx b/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx index eb8e52ba..fef3571b 100644 --- a/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx +++ b/grafana-plugin/src/containers/IncidentsFilters/IncidentsFilters.tsx @@ -417,8 +417,6 @@ class IncidentsFilters extends Component private pollingIntervalId: NodeJS.Timer = undefined; componentWillUnmount(): void { - clearInterval(this.pollingIntervalId); - this.pollingIntervalId = undefined; + this.clearPollingInterval(); } render() { From b84b174e2087b15be880145dc1bde7b37976b7c9 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 4 Oct 2022 09:25:53 +0100 Subject: [PATCH 78/89] Allow multiple database and celery broker types (#582) * add libs for celery + redis * move redis & cache config to settings/base.py * move rmq & celery config to settings/base.py * BROKER -> BROKER_TYPE * allow multiple database types * flake8 * add sqlite db creation to dockerfile * fix ci * fix ci * debug * remove some defaults * remove prints * use local memory as cache on ci * debug * add DATABASE_DEFAULTS * add ci test for sqlite + redis * add ci test for sqlite + redis * add ci test for sqlite + redis * debug * add redis healthcheck * fix sqlite * fix dev settings * refactor dev settings * tweak ci settings * clear cache properly between tests * move db and broker types to constants * add librabbitmq deps * use amqp instead of librabbitmq --- .github/workflows/ci.yml | 32 ++++- .gitignore | 1 + DEVELOPER.md | 4 +- engine/Dockerfile | 7 +- .../apps/integrations/tests/test_ratelimit.py | 9 +- engine/requirements.txt | 4 +- engine/settings/base.py | 130 ++++++++++++++++-- engine/settings/ci-test.py | 44 +++--- engine/settings/dev.py | 59 +++----- engine/settings/helm.py | 62 +-------- engine/settings/hobby.py | 35 +---- engine/settings/prod_without_db.py | 32 ----- 12 files changed, 204 insertions(+), 215 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d122096..e4b913c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: run: | docker run -v ${PWD}/docs/sources:/hugo/content/docs/oncall/latest -e HUGO_REFLINKSERRORLEVEL=ERROR --rm grafana/docs-base:latest /bin/bash -c 'make hugo' - unit-test-backend: + unit-test-backend-mysql-rabbitmq: runs-on: ubuntu-latest container: python:3.9 env: @@ -66,11 +66,11 @@ jobs: pip install -r requirements.txt ./wait_for_test_mysql_start.sh && pytest --ds=settings.ci-test -x - unit-test-backend-postgresql: + unit-test-backend-postgresql-rabbitmq: runs-on: ubuntu-latest container: python:3.9 env: - DB_BACKEND: postgresql + DATABASE_TYPE: postgresql DJANGO_SETTINGS_MODULE: settings.ci-test SLACK_CLIENT_OAUTH_ID: 1 services: @@ -98,3 +98,29 @@ jobs: pip install -r requirements.txt pytest --ds=settings.ci-test -x + unit-test-backend-sqlite-redis: + runs-on: ubuntu-latest + container: python:3.9 + env: + DATABASE_TYPE: sqlite3 + BROKER_TYPE: redis + REDIS_URI: redis://redis_test:6379 + DJANGO_SETTINGS_MODULE: settings.ci-test + SLACK_CLIENT_OAUTH_ID: 1 + services: + redis_test: + image: redis:7.0.5 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v2 + - name: Unit Test Backend + run: | + apt-get update && apt-get install -y netcat + cd engine/ + pip install -r requirements.txt + pytest --ds=settings.ci-test -x diff --git a/.gitignore b/.gitignore index 308f671f..d0748610 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Backend */db.sqlite3 +engine/oncall_dev.db *.pyc venv .python-version diff --git a/DEVELOPER.md b/DEVELOPER.md index fd71c96b..347f7e95 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -59,8 +59,8 @@ pip install -U pip wheel # Copy and check .env.dev file. cp .env.dev.example .env.dev -# NOTE: if you want to use the PostgreSQL db backend add DB_BACKEND=postgresql to your .env.dev file; -# currently allowed backend values are `mysql` (default) and `postgresql` +# NOTE: if you want to use the PostgreSQL db backend add DATABASE_TYPE=postgresql to your .env.dev file; +# currently allowed backend values are `mysql` (default), `postgresql` and `sqlite3` # Apply .env.dev to current terminal. # For PyCharm it's better to use https://plugins.jetbrains.com/plugin/7861-envfile/ diff --git a/engine/Dockerfile b/engine/Dockerfile index 4a736620..8a72ef39 100644 --- a/engine/Dockerfile +++ b/engine/Dockerfile @@ -9,8 +9,11 @@ RUN pip install -r requirements.txt COPY ./ ./ -RUN DJANGO_SETTINGS_MODULE=settings.prod_without_db SECRET_KEY="ThEmUsTSecretKEYforBUILDstage123" TELEGRAM_TOKEN="0000000000:XXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXX" SLACK_CLIENT_OAUTH_ID=1 python manage.py collectstatic --no-input -RUN rm db.sqlite3 +# Collect static files and create an SQLite database +RUN mkdir -p /var/lib/oncall +RUN DJANGO_SETTINGS_MODULE=settings.prod_without_db DATABASE_TYPE=sqlite3 DATABASE_NAME=/var/lib/oncall/oncall.db SECRET_KEY="ThEmUsTSecretKEYforBUILDstage123" python manage.py collectstatic --no-input +RUN chown -R 1000:2000 /var/lib/oncall + # This is required for prometheus_client to sync between uwsgi workers RUN mkdir -p /tmp/prometheus_django_metrics; diff --git a/engine/apps/integrations/tests/test_ratelimit.py b/engine/apps/integrations/tests/test_ratelimit.py index 75e3d903..97b56937 100644 --- a/engine/apps/integrations/tests/test_ratelimit.py +++ b/engine/apps/integrations/tests/test_ratelimit.py @@ -8,12 +8,9 @@ from django.urls import reverse from apps.alerts.models import AlertReceiveChannel -# Ratelimit keys are stored in cache. Clean it before and after every test to make them idempotent. -def setup_module(module): - cache.clear() - - -def teardown_module(module): +@pytest.fixture(autouse=True) +def clear_cache(): + # Ratelimit keys are stored in cache. Clean it before and after every test to make them idempotent. cache.clear() diff --git a/engine/requirements.txt b/engine/requirements.txt index 1bf66e51..deb82e56 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -5,8 +5,8 @@ whitenoise==5.3.0 twilio~=6.37.0 phonenumbers==8.10.0 django-ordered-model==3.1.1 -celery==5.2.7 -redis==3.2.0 +celery[amqp,redis]==5.2.7 +redis==3.4.1 humanize==0.5.1 uwsgi==2.0.20 django-cors-headers==3.7.0 diff --git a/engine/settings/base.py b/engine/settings/base.py index 3f893246..d9ec9f36 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -64,9 +64,6 @@ TWILIO_VERIFY_SERVICE_SID = os.environ.get("TWILIO_VERIFY_SERVICE_SID") TELEGRAM_WEBHOOK_HOST = os.environ.get("TELEGRAM_WEBHOOK_HOST", BASE_URL) TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN") -os.environ.setdefault("MYSQL_PASSWORD", "empty") -os.environ.setdefault("RABBIT_URI", "empty") - # For Sending email SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY") SENDGRID_FROM_EMAIL = os.environ.get("SENDGRID_FROM_EMAIL") @@ -84,21 +81,101 @@ GRAFANA_CLOUD_ONCALL_TOKEN = os.environ.get("GRAFANA_CLOUD_ONCALL_TOKEN", None) # Outgoing webhook settings DANGEROUS_WEBHOOKS_ENABLED = getenv_boolean("DANGEROUS_WEBHOOKS_ENABLED", default=False) -# DB backend defaults -DB_BACKEND = os.environ.get("DB_BACKEND", "mysql") -DB_BACKEND_DEFAULT_VALUES = { - "mysql": { + +# Database +class DatabaseTypes: + MYSQL = "mysql" + POSTGRESQL = "postgresql" + SQLITE3 = "sqlite3" + + +DATABASE_DEFAULTS = { + DatabaseTypes.MYSQL: { "USER": "root", - "PORT": "3306", + "PORT": 3306, + }, + DatabaseTypes.POSTGRESQL: { + "USER": "postgres", + "PORT": 5432, + }, +} + +DATABASE_NAME = os.getenv("DATABASE_NAME") or os.getenv("MYSQL_DB_NAME") +DATABASE_USER = os.getenv("DATABASE_USER") or os.getenv("MYSQL_USER") +DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD") or os.getenv("MYSQL_PASSWORD") +DATABASE_HOST = os.getenv("DATABASE_HOST") or os.getenv("MYSQL_HOST") +DATABASE_PORT = os.getenv("DATABASE_PORT") or os.getenv("MYSQL_PORT") + +DATABASE_TYPE = os.getenv("DATABASE_TYPE", DatabaseTypes.MYSQL).lower() +assert DATABASE_TYPE in {DatabaseTypes.MYSQL, DatabaseTypes.POSTGRESQL, DatabaseTypes.SQLITE3} + +DATABASE_ENGINE = f"django.db.backends.{DATABASE_TYPE}" + +DATABASE_CONFIGS = { + DatabaseTypes.SQLITE3: { + "ENGINE": DATABASE_ENGINE, + "NAME": DATABASE_NAME or "/var/lib/oncall/oncall.db", + }, + DatabaseTypes.MYSQL: { + "ENGINE": DATABASE_ENGINE, + "NAME": DATABASE_NAME, + "USER": DATABASE_USER, + "PASSWORD": DATABASE_PASSWORD, + "HOST": DATABASE_HOST, + "PORT": DATABASE_PORT, "OPTIONS": { "charset": "utf8mb4", "connect_timeout": 1, }, }, - "postgresql": { - "USER": "postgres", - "PORT": "5432", - "OPTIONS": {}, + DatabaseTypes.POSTGRESQL: { + "ENGINE": DATABASE_ENGINE, + "NAME": DATABASE_NAME, + "USER": DATABASE_USER, + "PASSWORD": DATABASE_PASSWORD, + "HOST": DATABASE_HOST, + "PORT": DATABASE_PORT, + }, +} + +DATABASES = { + "default": DATABASE_CONFIGS[DATABASE_TYPE], +} +if DATABASE_TYPE == DatabaseTypes.MYSQL: + # Workaround to use pymysql instead of mysqlclient + import pymysql + + pymysql.install_as_MySQLdb() + +# Redis +REDIS_USERNAME = os.getenv("REDIS_USERNAME", "") +REDIS_PASSWORD = os.getenv("REDIS_PASSWORD") +REDIS_HOST = os.getenv("REDIS_HOST") +REDIS_PORT = os.getenv("REDIS_PORT", 6379) +REDIS_PROTOCOL = os.getenv("REDIS_PROTOCOL", "redis") + +REDIS_URI = os.getenv("REDIS_URI") +if not REDIS_URI: + REDIS_URI = f"{REDIS_PROTOCOL}://{REDIS_USERNAME}:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}" + +# Cache +CACHES = { + "default": { + "BACKEND": "redis_cache.RedisCache", + "LOCATION": [ + REDIS_URI, + ], + "OPTIONS": { + "DB": 1, + "PARSER_CLASS": "redis.connection.HiredisParser", + "CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool", + "CONNECTION_POOL_CLASS_KWARGS": { + "max_connections": 50, + "timeout": 20, + }, + "MAX_CONNECTIONS": 1000, + "PICKLE_VERSION": -1, + }, }, } @@ -261,7 +338,34 @@ USE_TZ = True STATIC_URL = os.environ.get("STATIC_URL", "/static/") STATIC_ROOT = "./static/" -CELERY_BROKER_URL = "amqp://rabbitmq:rabbitmq@localhost:5672" +# RabbitMQ +RABBITMQ_USERNAME = os.getenv("RABBITMQ_USERNAME") +RABBITMQ_PASSWORD = os.getenv("RABBITMQ_PASSWORD") +RABBITMQ_HOST = os.getenv("RABBITMQ_HOST") +RABBITMQ_PORT = os.getenv("RABBITMQ_PORT", 5672) +RABBITMQ_PROTOCOL = os.getenv("RABBITMQ_PROTOCOL", "amqp") +RABBITMQ_VHOST = os.getenv("RABBITMQ_VHOST", "") + +RABBITMQ_URI = os.getenv("RABBITMQ_URI") or os.getenv("RABBIT_URI") +if not RABBITMQ_URI: + RABBITMQ_URI = f"{RABBITMQ_PROTOCOL}://{RABBITMQ_USERNAME}:{RABBITMQ_PASSWORD}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST}" + + +# Celery +class BrokerTypes: + RABBITMQ = "rabbitmq" + REDIS = "redis" + + +BROKER_TYPE = os.getenv("BROKER_TYPE", BrokerTypes.RABBITMQ).lower() +assert BROKER_TYPE in {BrokerTypes.RABBITMQ, BrokerTypes.REDIS} + +if BROKER_TYPE == BrokerTypes.RABBITMQ: + CELERY_BROKER_URL = RABBITMQ_URI +elif BROKER_TYPE == BrokerTypes.REDIS: + CELERY_BROKER_URL = REDIS_URI +else: + raise ValueError(f"Invalid BROKER_TYPE env variable: {BROKER_TYPE}") # By default, apply_async will just hang indefinitely trying to reach to RabbitMQ even if RabbitMQ is down. # This makes apply_async retry 3 times trying to reach to RabbitMQ, with some extra info on periods between retries. diff --git a/engine/settings/ci-test.py b/engine/settings/ci-test.py index b3d39d4e..7af883d3 100644 --- a/engine/settings/ci-test.py +++ b/engine/settings/ci-test.py @@ -1,6 +1,6 @@ -# flake8: noqa: F405 +# flake8: noqa -from .base import * # noqa +from .base import * SECRET_KEY = "u5/IIbuiJR3Y9FQMBActk+btReZ5oOxu+l8MIJQWLfVzESoan5REE6UNSYYEQdjBOcty9CDak2X" @@ -9,27 +9,29 @@ MIRAGE_CIPHER_IV = "X+VFcDqtxJ5bbU+V" BASE_URL = "http://localhost" -CELERY_BROKER_URL = "amqp://rabbitmq:rabbitmq@rabbit_test:5672" +if DATABASE_TYPE == DatabaseTypes.SQLITE3: + DATABASES["default"]["NAME"] = DATABASE_NAME or "oncall_ci.db" +else: + DATABASES["default"] |= { + "NAME": DATABASE_NAME or "oncall_local_dev", + "USER": DATABASE_USER or DATABASE_DEFAULTS[DATABASE_TYPE]["USER"], + "PASSWORD": DATABASE_PASSWORD or "local_dev_pwd", + "HOST": DATABASE_HOST or f"{DATABASE_TYPE}_test", + "PORT": DATABASE_PORT or DATABASE_DEFAULTS[DATABASE_TYPE]["PORT"], + } -if DB_BACKEND == "mysql": - # Workaround to use pymysql instead of mysqlclient - import pymysql +if BROKER_TYPE == BrokerTypes.RABBITMQ: + CELERY_BROKER_URL = "amqp://rabbitmq:rabbitmq@rabbit_test:5672" +elif BROKER_TYPE == BrokerTypes.REDIS: + CELERY_BROKER_URL = REDIS_URI - pymysql.install_as_MySQLdb() - DB_BACKEND_DEFAULT_VALUES[DB_BACKEND]["OPTIONS"] = {"charset": "utf8mb4"} - - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.{}".format(DB_BACKEND), - "NAME": os.environ.get("DB_NAME", "oncall_local_dev"), - "USER": os.environ.get("DB_USER", DB_BACKEND_DEFAULT_VALUES.get(DB_BACKEND, {}).get("USER", "root")), - "PASSWORD": "local_dev_pwd", - "HOST": "{}_test".format(DB_BACKEND), - "PORT": os.environ.get("DB_PORT", DB_BACKEND_DEFAULT_VALUES.get(DB_BACKEND, {}).get("PORT", "3306")), - "OPTIONS": DB_BACKEND_DEFAULT_VALUES.get(DB_BACKEND, {}).get("OPTIONS", {}), - }, -} +# use redis as cache and celery broker on CI tests +if BROKER_TYPE != BrokerTypes.REDIS: + CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + } + } # Dummy Telegram token (fake one) TELEGRAM_TOKEN = "0000000000:XXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXX" diff --git a/engine/settings/dev.py b/engine/settings/dev.py index fb7ddc3e..9c418ae4 100644 --- a/engine/settings/dev.py +++ b/engine/settings/dev.py @@ -1,27 +1,28 @@ +# flake8: noqa import os import sys -from .base import * # noqa - -if DB_BACKEND == "mysql": # noqa - # Workaround to use pymysql instead of mysqlclient - import pymysql - - pymysql.install_as_MySQLdb() +from .base import * DEBUG = True -DATABASES = { - "default": { - "ENGINE": "django.db.backends.{}".format(DB_BACKEND), # noqa - "NAME": os.environ.get("DB_NAME", "oncall_local_dev"), - "USER": os.environ.get("DB_USER", DB_BACKEND_DEFAULT_VALUES.get(DB_BACKEND, {}).get("USER", "root")), # noqa - "PASSWORD": os.environ.get("DB_PASSWORD", "empty"), - "HOST": os.environ.get("DB_HOST", "127.0.0.1"), - "PORT": os.environ.get("DB_PORT", DB_BACKEND_DEFAULT_VALUES.get(DB_BACKEND, {}).get("PORT", "3306")), # noqa - "OPTIONS": DB_BACKEND_DEFAULT_VALUES.get(DB_BACKEND, {}).get("OPTIONS", {}), # noqa - }, -} +if DATABASE_TYPE == DatabaseTypes.SQLITE3: + DATABASES["default"]["NAME"] = DATABASE_NAME or "oncall_dev.db" +else: + DATABASES["default"] |= { + "NAME": DATABASE_NAME or "oncall_local_dev", + "USER": DATABASE_USER or DATABASE_DEFAULTS[DATABASE_TYPE]["USER"], + "PASSWORD": DATABASE_PASSWORD or "empty", + "HOST": DATABASE_HOST or "127.0.0.1", + "PORT": DATABASE_PORT or DATABASE_DEFAULTS[DATABASE_TYPE]["PORT"], + } + +if BROKER_TYPE == BrokerTypes.RABBITMQ: + CELERY_BROKER_URL = "pyamqp://rabbitmq:rabbitmq@localhost:5672" +elif BROKER_TYPE == BrokerTypes.REDIS: + CELERY_BROKER_URL = "redis://localhost:6379" + +CACHES["default"]["LOCATION"] = ["localhost:6379"] SECRET_KEY = os.environ.get("SECRET_KEY", "osMsNM0PqlRHBlUvqmeJ7+ldU3IUETCrY9TrmiViaSmInBHolr1WUlS0OFS4AHrnnkp1vp9S9z1") @@ -32,28 +33,6 @@ MIRAGE_CIPHER_IV = os.environ.get("MIRAGE_CIPHER_IV", "tZZa+60zTZO2NRcS") TESTING = "pytest" in sys.modules or "unittest" in sys.modules -CACHES = { - "default": { - "BACKEND": "redis_cache.RedisCache", - "LOCATION": [ - "localhost:6379", - ], - "OPTIONS": { - "DB": 1, - "PARSER_CLASS": "redis.connection.HiredisParser", - "CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool", - "CONNECTION_POOL_CLASS_KWARGS": { - "max_connections": 50, - "timeout": 20, - }, - "MAX_CONNECTIONS": 1000, - "PICKLE_VERSION": -1, - }, - }, -} - -CELERY_BROKER_URL = "pyamqp://rabbitmq:rabbitmq@localhost:5672" - SILKY_PYTHON_PROFILER = True # For any requests that come in with that header/value, request.is_secure() will return True. diff --git a/engine/settings/helm.py b/engine/settings/helm.py index 00fa96c3..6ae28e8a 100644 --- a/engine/settings/helm.py +++ b/engine/settings/helm.py @@ -1,64 +1,4 @@ -import os - -# Workaround to use pymysql instead of mysqlclient -import pymysql - -from .prod_without_db import * # noqa - -pymysql.install_as_MySQLdb() - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.mysql", - "NAME": os.environ.get("MYSQL_DB_NAME"), - "USER": os.environ.get("MYSQL_USER"), - "PASSWORD": os.environ["MYSQL_PASSWORD"], - "HOST": os.environ.get("MYSQL_HOST"), - "PORT": os.environ.get("MYSQL_PORT"), - "OPTIONS": { - "charset": "utf8mb4", - "connect_timeout": 1, - }, - }, -} - -RABBITMQ_USERNAME = os.environ.get("RABBITMQ_USERNAME") -RABBITMQ_PASSWORD = os.environ.get("RABBITMQ_PASSWORD") -RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST") -RABBITMQ_PORT = os.environ.get("RABBITMQ_PORT") -RABBITMQ_PROTOCOL = os.environ.get("RABBITMQ_PROTOCOL") -RABBITMQ_VHOST = os.environ.get("RABBITMQ_VHOST", "") - -CELERY_BROKER_URL = ( - f"{RABBITMQ_PROTOCOL}://{RABBITMQ_USERNAME}:{RABBITMQ_PASSWORD}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST}" -) - -REDIS_USERNAME = os.environ.get("REDIS_USERNAME", "") -REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD") -REDIS_HOST = os.environ.get("REDIS_HOST") -REDIS_PORT = os.environ.get("REDIS_PORT", "6379") -REDIS_PROTOCOL = os.environ.get("REDIS_PROTOCOL", "redis") -REDIS_URI = f"{REDIS_PROTOCOL}://{REDIS_USERNAME}:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}" - -CACHES = { - "default": { - "BACKEND": "redis_cache.RedisCache", - "LOCATION": [ - REDIS_URI, - ], - "OPTIONS": { - "DB": 1, - "PARSER_CLASS": "redis.connection.HiredisParser", - "CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool", - "CONNECTION_POOL_CLASS_KWARGS": { - "max_connections": 50, - "timeout": 20, - }, - "MAX_CONNECTIONS": 1000, - "PICKLE_VERSION": -1, - }, - }, -} +from .prod_without_db import * # noqa: F401, F403 APPEND_SLASH = False SECURE_SSL_REDIRECT = False diff --git a/engine/settings/hobby.py b/engine/settings/hobby.py index 3bd73c13..ca7299b0 100644 --- a/engine/settings/hobby.py +++ b/engine/settings/hobby.py @@ -1,37 +1,6 @@ -# flake8: noqa: F405 +from .prod_without_db import * # noqa: F403 -from random import randrange - -# Workaround to use pymysql instead of mysqlclient -import pymysql - -from .prod_without_db import * # noqa - -pymysql.install_as_MySQLdb() - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.mysql", - "NAME": os.environ.get("MYSQL_DB_NAME"), - "USER": os.environ.get("MYSQL_USER"), - "PASSWORD": os.environ["MYSQL_PASSWORD"], - "HOST": os.environ.get("MYSQL_HOST"), - "PORT": os.environ.get("MYSQL_PORT"), - "OPTIONS": { - "charset": "utf8mb4", - "connect_timeout": 1, - }, - }, -} - -RABBITMQ_USERNAME = os.environ.get("RABBITMQ_USERNAME") -RABBITMQ_PASSWORD = os.environ.get("RABBITMQ_PASSWORD") -RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST") -RABBITMQ_PORT = os.environ.get("RABBITMQ_PORT") - -CELERY_BROKER_URL = f"amqp://{RABBITMQ_USERNAME}:{RABBITMQ_PASSWORD}@{RABBITMQ_HOST}:{RABBITMQ_PORT}" - -MIRAGE_SECRET_KEY = SECRET_KEY +MIRAGE_SECRET_KEY = SECRET_KEY # noqa: F405 MIRAGE_CIPHER_IV = "1234567890abcdef" # use default APPEND_SLASH = False diff --git a/engine/settings/prod_without_db.py b/engine/settings/prod_without_db.py index 6b7c20d8..0c583483 100644 --- a/engine/settings/prod_without_db.py +++ b/engine/settings/prod_without_db.py @@ -15,36 +15,6 @@ except ModuleNotFoundError: from .base import * # noqa -# It's required for collectstatic to avoid connecting it to MySQL - -# Primary database must have the name "default" -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), # noqa - } -} - -CACHES = { - "default": { - "BACKEND": "redis_cache.RedisCache", - "LOCATION": [ - os.environ.get("REDIS_URI"), - ], - "OPTIONS": { - "DB": 1, - "PARSER_CLASS": "redis.connection.HiredisParser", - "CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool", - "CONNECTION_POOL_CLASS_KWARGS": { - "max_connections": 50, - "timeout": 20, - }, - "MAX_CONNECTIONS": 1000, - "PICKLE_VERSION": -1, - }, - }, -} - SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET") SLACK_SIGNING_SECRET_LIVE = os.environ.get("SLACK_SIGNING_SECRET_LIVE", "") @@ -56,8 +26,6 @@ STATIC_ROOT = "./collected_static/" DEBUG = False -CELERY_BROKER_URL = os.environ["RABBIT_URI"] - SECURE_SSL_REDIRECT = True SECURE_REDIRECT_EXEMPT = [ "^health/", From e32eecf6ff72b743042049b8120cede7b4ff0fc2 Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Tue, 4 Oct 2022 16:48:33 +0800 Subject: [PATCH 79/89] Fix code styling in helm chart --- helm/oncall/values.yaml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/helm/oncall/values.yaml b/helm/oncall/values.yaml index 7ab74f2c..f0a02773 100644 --- a/helm/oncall/values.yaml +++ b/helm/oncall/values.yaml @@ -29,18 +29,17 @@ engine: # cpu: 100m # memory: 128Mi - ## @param affinity Affinity for pod assignment - ## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity - ## - affinity: { } - ## @param nodeSelector Node labels for pod assignment + ## Affinity for pod assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + affinity: {} + + ## Node labels for pod assignment ## ref: https://kubernetes.io/docs/user-guide/node-selection/ - ## - nodeSelector: { } - ## @param tolerations Tolerations for pod assignment + nodeSelector: {} + + ## Tolerations for pod assignment ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ - ## - tolerations: [ ] + tolerations: [] # Celery workers pods configuration celery: From 2bc36440e61c550ddb9581642927f3b011dc4e3f Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Tue, 4 Oct 2022 16:58:35 +0800 Subject: [PATCH 80/89] Bump cryptography vertsion --- engine/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/requirements.txt b/engine/requirements.txt index deb82e56..950c1d1e 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -24,7 +24,7 @@ slack-export-viewer==1.0.0 beautifulsoup4==4.8.1 social-auth-app-django==3.1.0 sendgrid==6.1.2 -cryptography==3.2 +cryptography==3.3.2 pytest==5.4.3 pytest-django==3.9.0 pytest_factoryboy==2.0.3 From a7c37b2fba0f39e0b87912299d259e8f6aa30fce Mon Sep 17 00:00:00 2001 From: Gilberto Junior Date: Tue, 4 Oct 2022 06:19:13 -0300 Subject: [PATCH 81/89] Pagerduty migrator/add scripts (#403) * Script to import users from pagerduty to Grafana * Added README * Update tools/pagerduty-migrator/scripts/README.md Co-authored-by: Vadim Stepanov * Update tools/pagerduty-migrator/scripts/add_users_pagerduty_to_grafana.py Co-authored-by: Vadim Stepanov * Update tools/pagerduty-migrator/scripts/add_users_pagerduty_to_grafana.py Co-authored-by: Vadim Stepanov * Update tools/pagerduty-migrator/scripts/add_users_pagerduty_to_grafana.py Co-authored-by: Vadim Stepanov * Update tools/pagerduty-migrator/scripts/add_users_pagerduty_to_grafana.py Co-authored-by: Vadim Stepanov * Update tools/pagerduty-migrator/scripts/add_users_pagerduty_to_grafana.py Co-authored-by: Vadim Stepanov * Adjusted script to follow project pattern * Changed variable name. * Guidance for running the script Co-authored-by: Vadim Stepanov --- tools/pagerduty-migrator/scripts/README.md | 12 ++++++ .../scripts/add_users_pagerduty_to_grafana.py | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tools/pagerduty-migrator/scripts/README.md create mode 100644 tools/pagerduty-migrator/scripts/add_users_pagerduty_to_grafana.py diff --git a/tools/pagerduty-migrator/scripts/README.md b/tools/pagerduty-migrator/scripts/README.md new file mode 100644 index 00000000..c1aab511 --- /dev/null +++ b/tools/pagerduty-migrator/scripts/README.md @@ -0,0 +1,12 @@ +# PagerDuty migrator scripts + +When we run MODE="plan" we can notice that there is escalation, integration in pagerduty that needs to be linked to a user. + +To solve this problem, we can run the add_users_pagerduty_to_grafana.py script + +```bash +docker run -it --rm -e PAGERDUTY_API_TOKEN="mytoken" -e GRAFANA_URL="http://localhost:3000" -e GRAFANA_USERNAME="admin" -e GRAFANA_PASSWORD="admin" pd-oncall-migrator python /app/scripts/add_users_pagerduty_to_grafana.py +``` + +It is worth remembering that this script will create a user with a random password. +To access with the user created, it will be necessary to change the password in grafana web. \ No newline at end of file diff --git a/tools/pagerduty-migrator/scripts/add_users_pagerduty_to_grafana.py b/tools/pagerduty-migrator/scripts/add_users_pagerduty_to_grafana.py new file mode 100644 index 00000000..28aa542e --- /dev/null +++ b/tools/pagerduty-migrator/scripts/add_users_pagerduty_to_grafana.py @@ -0,0 +1,42 @@ +import os +import secrets +import sys +import requests + +from urllib.parse import urljoin +from pdpyras import APISession + +PAGERDUTY_API_TOKEN = os.environ["PAGERDUTY_API_TOKEN"] +PATH_USERS_GRAFANA = "/api/admin/users" +GRAFANA_URL = os.environ["GRAFANA_URL"] # Example: http://localhost:3000 +GRAFANA_USERNAME = os.environ["GRAFANA_USERNAME"] +GRAFANA_PASSWORD = os.environ["GRAFANA_PASSWORD"] +SUCCESS_SIGN = "✅" +ERROR_SIGN = "❌" + +def list_pagerduty_users(): + session = APISession(PAGERDUTY_API_TOKEN) + + users = session.list_all("users") + + for user in users: + password = secrets.token_urlsafe(15) + username = user["email"].split("@")[0] + json = {"name": user["name"], "email": user["email"], "login": username, "password": password} + create_grafana_user(json) + +def create_grafana_user(data): + url = urljoin(GRAFANA_URL, PATH_USERS_GRAFANA) + response = requests.request("POST", url, auth=(GRAFANA_USERNAME, GRAFANA_PASSWORD), json=data) + + if response.status_code == 200: + print(SUCCESS_SIGN + " User created: " + data["login"]) + elif response.status_code == 401: + sys.exit(ERROR_SIGN + " Invalid username or password.") + elif response.status_code == 412: + print(ERROR_SIGN + " User " + data["login"] + " already exists." ) + else: + print("{} {}".format(ERROR_SIGN, response.text)) + +if __name__ == "__main__": + list_pagerduty_users() \ No newline at end of file From a96f7315d46c4f883500e8d5bd3990115bfbe753 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 4 Oct 2022 12:55:19 +0100 Subject: [PATCH 82/89] fix constructSyncErrorMessage for empty JsonData --- .../src/containers/PluginConfigPage/PluginConfigPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx index e199bf02..ab5eb863 100644 --- a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx +++ b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx @@ -130,7 +130,7 @@ export const PluginConfigPage = (props: Props) => { const handleSyncException = useCallback((e) => { const buildErrMsg = (msg: string): string => - constructSyncErrorMessage(msg, plugin.meta.jsonData.onCallApiUrl); + constructSyncErrorMessage(msg, plugin.meta.jsonData?.onCallApiUrl); if (plugin.meta.jsonData?.onCallApiUrl) { const { status: statusCode } = e.response; From 96520381b1f73bdaffbeaef322d2dd45c2b521dd Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Tue, 4 Oct 2022 10:20:12 -0700 Subject: [PATCH 83/89] Update helm/oncall/templates/ingress-regular.yaml Co-authored-by: Ildar Iskhakov --- helm/oncall/templates/ingress-regular.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/oncall/templates/ingress-regular.yaml b/helm/oncall/templates/ingress-regular.yaml index 61a8401a..d29756aa 100644 --- a/helm/oncall/templates/ingress-regular.yaml +++ b/helm/oncall/templates/ingress-regular.yaml @@ -25,7 +25,7 @@ metadata: spec: {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} ingressClassName: {{ .Values.ingress.className }} - {{- end}} + {{- end }} {{- if .Values.ingress.tls }} tls: {{- tpl (toYaml .Values.ingress.tls) . | nindent 4 }} From f56d82c5dbd9ccdf5f5ee014f97d0e4a88e6a1fb Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 4 Oct 2022 15:27:24 -0600 Subject: [PATCH 84/89] Use exponential backoff for sync retry to improve latency instead of waiting 2s between attempts --- .../PluginConfigPage/PluginConfigPage.tsx | 59 ++++++++++--------- grafana-plugin/src/state/plugin.ts | 5 ++ grafana-plugin/src/state/rootBaseStore.ts | 52 ++++++++-------- 3 files changed, 64 insertions(+), 52 deletions(-) diff --git a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx index ab5eb863..d10ed6f6 100644 --- a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx +++ b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx @@ -20,7 +20,13 @@ import Text from 'components/Text/Text'; import WithConfirm from 'components/WithConfirm/WithConfirm'; import logo from 'img/logo.svg'; import { makeRequest } from 'network'; -import { createGrafanaToken, getPluginSyncStatus, startPluginSync, updateGrafanaToken } from 'state/plugin'; +import { + createGrafanaToken, + getPluginSyncStatus, + startPluginSync, SYNC_STATUS_RETRY_LIMIT, + syncStatusDelay, + updateGrafanaToken +} from 'state/plugin'; import { GRAFANA_LICENSE_OSS } from 'utils/consts'; import { getItem, setItem } from 'utils/localStorage'; @@ -178,38 +184,33 @@ export const PluginConfigPage = (props: Props) => { setPluginConfigLoading(false); }, []); + const waitForSyncStatus = (retryCount = 0) => { + if (retryCount > SYNC_STATUS_RETRY_LIMIT) { + setPluginStatusMessage( + `OnCall took too many tries to synchronize. Did you launch Celery workers? Background workers should perform synchronization, not web server.` + ); + setRetrySync(true); + setPluginStatusOk(false); + setPluginConfigLoading(false); + return; + } + + getPluginSyncStatus().then((get_sync_response) => { + if (get_sync_response.hasOwnProperty('token_ok')) { + finishSync(get_sync_response); + } else { + syncStatusDelay(retryCount + 1).then(() => waitForSyncStatus(retryCount + 1)) + } + }).catch((e) => { + handleSyncException(e); + }); + } + const startSync = useCallback(() => { setRetrySync(false); setPluginConfigLoading(true); startPluginSync() - .then(() => { - let counter = 0; - const interval = setInterval(() => { - counter++; - - getPluginSyncStatus() - .then((get_sync_response) => { - if (get_sync_response.hasOwnProperty('token_ok')) { - clearInterval(interval); - finishSync(get_sync_response); - } - }) - .catch((e) => { - clearInterval(interval); - handleSyncException(e); - }); - - if (counter >= 5) { - clearInterval(interval); - setPluginStatusMessage( - `OnCall took too many tries to synchronize. Did you launch Celery workers? Background workers should perform synchronization, not web server.` - ); - setRetrySync(true); - setPluginStatusOk(false); - setPluginConfigLoading(false); - } - }, 2000); - }) + .then(() => waitForSyncStatus()) .catch(handleSyncException); }, []); diff --git a/grafana-plugin/src/state/plugin.ts b/grafana-plugin/src/state/plugin.ts index 755c3b37..544eda02 100644 --- a/grafana-plugin/src/state/plugin.ts +++ b/grafana-plugin/src/state/plugin.ts @@ -31,6 +31,11 @@ export async function startPluginSync() { return await makeRequest('/plugin/sync', { method: 'POST' }); } + +export const SYNC_STATUS_RETRY_LIMIT = 10; + +export const syncStatusDelay = retryCount => new Promise(resolve => setTimeout(resolve, 10 * 2 ** retryCount)); + export async function getPluginSyncStatus() { return await makeRequest(`/plugin/sync`, { method: 'GET' }); } diff --git a/grafana-plugin/src/state/rootBaseStore.ts b/grafana-plugin/src/state/rootBaseStore.ts index 605ba9f8..ce4ebb1d 100644 --- a/grafana-plugin/src/state/rootBaseStore.ts +++ b/grafana-plugin/src/state/rootBaseStore.ts @@ -31,7 +31,14 @@ import { UserGroupStore } from 'models/user_group/user_group'; import { makeRequest } from 'network'; import { AppFeature } from './features'; -import { createGrafanaToken, getPluginSyncStatus, installPlugin, startPluginSync, updateGrafanaToken } from './plugin'; +import { + createGrafanaToken, + getPluginSyncStatus, + installPlugin, + startPluginSync, + SYNC_STATUS_RETRY_LIMIT, syncStatusDelay, + updateGrafanaToken +} from './plugin'; import { UserAction } from './userAction'; // ------ Dashboard ------ // @@ -182,6 +189,26 @@ export class RootBaseStore { this.isUserAnonymous = false; } + async waitForSyncStatus(retryCount = 0) { + + if (retryCount > SYNC_STATUS_RETRY_LIMIT) { + this.retrySync = true; + return; + } + + getPluginSyncStatus().then((get_sync_response) => { + if (get_sync_response.hasOwnProperty('token_ok')) { + this.finishSync(get_sync_response); + } else { + syncStatusDelay(retryCount + 1) + .then(() => this.waitForSyncStatus(retryCount + 1)) + } + }).catch((e) => { + this.handleSyncException(e); + }); + + } + async setupPlugin(meta: AppPluginMeta) { this.resetStatusToDefault(); @@ -208,28 +235,7 @@ export class RootBaseStore { } await installPlugin(); } - - let counter = 0; - const interval = setInterval(() => { - counter++; - - getPluginSyncStatus() - .then((get_sync_response) => { - if (get_sync_response.hasOwnProperty('token_ok')) { - clearInterval(interval); - this.finishSync(get_sync_response); - } - }) - .catch((e) => { - clearInterval(interval); - this.handleSyncException(e); - }); - - if (counter >= 10) { - clearInterval(interval); - this.retrySync = true; - } - }, 2000); + await this.waitForSyncStatus(); } isUserActionAllowed(action: UserAction) { From b6092f6d5ce2ff93ff66170566641e719a9c780f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juris=20Pav=C4=BCu=C4=8Denkovs?= Date: Wed, 5 Oct 2022 11:11:08 +0300 Subject: [PATCH 85/89] fix helm celery healthcheck --- helm/oncall/templates/celery/_deployment.tpl | 10 ++++++---- helm/oncall/values.yaml | 5 +++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/helm/oncall/templates/celery/_deployment.tpl b/helm/oncall/templates/celery/_deployment.tpl index 45e50a0d..5378c834 100644 --- a/helm/oncall/templates/celery/_deployment.tpl +++ b/helm/oncall/templates/celery/_deployment.tpl @@ -47,16 +47,18 @@ spec: {{- if .Values.env }} {{- toYaml .Values.env | nindent 12 }} {{- end }} + {{- if .Values.celery.livenessProbe.enabled }} livenessProbe: exec: command: [ "bash", "-c", - "celery inspect ping -A engine -d celery@$HOSTNAME" + "celery -A engine inspect ping -d celery@$HOSTNAME" ] - initialDelaySeconds: 30 - periodSeconds: 300 - timeoutSeconds: 10 + initialDelaySeconds: {{ .Values.celery.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.celery.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.celery.livenessProbe.timeoutSeconds }} + {{- end }} resources: {{- toYaml .Values.celery.resources | nindent 12 }} {{- end}} \ No newline at end of file diff --git a/helm/oncall/values.yaml b/helm/oncall/values.yaml index f0a02773..700bccb8 100644 --- a/helm/oncall/values.yaml +++ b/helm/oncall/values.yaml @@ -44,6 +44,11 @@ engine: # Celery workers pods configuration celery: replicaCount: 1 + livenessProbe: + enabled: false + initialDelaySeconds: 30 + periodSeconds: 300 + timeoutSeconds: 10 resources: {} # limits: # cpu: 100m From f4d79e634a2d3df7ce16c430ef768e9b82cb230b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juris=20Pav=C4=BCu=C4=8Denkovs?= Date: Wed, 5 Oct 2022 11:23:25 +0300 Subject: [PATCH 86/89] enable celery liveness probe --- helm/oncall/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/oncall/values.yaml b/helm/oncall/values.yaml index 700bccb8..87e2516f 100644 --- a/helm/oncall/values.yaml +++ b/helm/oncall/values.yaml @@ -45,7 +45,7 @@ engine: celery: replicaCount: 1 livenessProbe: - enabled: false + enabled: true initialDelaySeconds: 30 periodSeconds: 300 timeoutSeconds: 10 From f378eab7b790fc04b27d1ba0a2153b706791ad74 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Wed, 5 Oct 2022 12:42:08 -0300 Subject: [PATCH 87/89] Allow enabling schedules alpha per organization --- engine/apps/api/views/features.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/engine/apps/api/views/features.py b/engine/apps/api/views/features.py index fc56ca93..cc69514f 100644 --- a/engine/apps/api/views/features.py +++ b/engine/apps/api/views/features.py @@ -27,6 +27,7 @@ class FeaturesAPIView(APIView): return Response(self._get_enabled_features(request)) def _get_enabled_features(self, request): + DynamicSetting = apps.get_model("base", "DynamicSetting") enabled_features = [] if settings.FEATURE_SLACK_INTEGRATION_ENABLED: @@ -36,7 +37,6 @@ class FeaturesAPIView(APIView): enabled_features.append(FEATURE_TELEGRAM) if settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED: - DynamicSetting = apps.get_model("base", "DynamicSetting") mobile_app_settings = DynamicSetting.objects.get_or_create( name="mobile_app_settings", defaults={ @@ -59,5 +59,17 @@ class FeaturesAPIView(APIView): if settings.FEATURE_WEB_SCHEDULES_ENABLED: enabled_features.append(FEATURE_WEB_SCHEDULES) + else: + # allow enabling web schedules per org, independently of global status flag + enabled_web_schedules_orgs = DynamicSetting.objects.get_or_create( + name="enabled_web_schedules_orgs", + defaults={ + "json_value": { + "org_ids": [], + } + }, + )[0] + if request.auth.organization.pk in enabled_web_schedules_orgs.json_value["org_ids"]: + enabled_features.append(FEATURE_WEB_SCHEDULES) return enabled_features From f65430fbcaf910bf1153d658c059ad2d518eaa9f Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Wed, 5 Oct 2022 14:55:33 -0300 Subject: [PATCH 88/89] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebfa6fb2..01e27157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## v1.0.40 (2022-10-05) +- Improved database and celery backends support +- Added script to import PagerDuty users to Grafana +- Bug fixes + ## v1.0.39 (2022-10-03) - Fix issue in v1.0.38 blocking the creation of schedules and webhooks in the UI From aedb6a52d53d714da1563253d32cc955b47f5e93 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Tue, 11 Oct 2022 16:08:43 +0200 Subject: [PATCH 89/89] Changed the order of concating values --- grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx b/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx index 675357e9..4f281542 100644 --- a/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx +++ b/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx @@ -61,8 +61,8 @@ const RemoteSelect = inject('store')( }; function mergeOptions(oldOptions: SelectableValue[], newOptions: SelectableValue[]) { - const existedValues = oldOptions.map((o) => o.value); - return newOptions.filter(({ value }) => !existedValues.includes(value)).concat(oldOptions); + const existingValues = oldOptions.map((o) => o.value); + return oldOptions.concat(newOptions.filter(({ value }) => !existingValues.includes(value))); } const [options, setOptions] = useReducer(mergeOptions, []);