From 24aa3a5c83080679ee5c25dfa9aa4bd6786c46e3 Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Mon, 8 Jul 2024 12:02:04 +0800 Subject: [PATCH 1/9] Add filtering by team, is_currently_oncall and search on the user page (#4575) # What this PR does This PR adds filtering by team and is_currently_oncall on the user page ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall/issues/4353 ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- Tiltfile | 2 +- engine/apps/api/views/user.py | 41 +++++++++ .../e2e-tests/users/usersActions.test.ts | 10 ++- .../src/models/user/user.helpers.tsx | 5 +- grafana-plugin/src/models/user/user.ts | 7 +- .../src/pages/users/Users.styles.ts | 4 + grafana-plugin/src/pages/users/Users.tsx | 87 ++++++++----------- 7 files changed, 97 insertions(+), 59 deletions(-) diff --git a/Tiltfile b/Tiltfile index ebb036dc..a2cd3a51 100644 --- a/Tiltfile +++ b/Tiltfile @@ -46,7 +46,7 @@ docker_build_sub( "localhost:63628/oncall/engine:dev", context="./engine", cache_from=["grafana/oncall:latest", "grafana/oncall:dev"], - ignore=["./test-results/", "./grafana-plugin/dist/", "./grafana-plugin/e2e-tests/"], + ignore=["./test-results/", "./grafana-plugin/dist/", "./grafana-plugin/e2e-tests/", "./grafana-plugin/node_modules/"], child_context=".", target="dev", extra_cmds=["ADD ./grafana-plugin/src/plugin.json /etc/grafana-plugin/src/plugin.json"], diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index 940e08a6..85b81825 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -235,6 +235,7 @@ class UserView( "send_test_sms": [RBACPermission.Permissions.USER_SETTINGS_WRITE], "export_token": [RBACPermission.Permissions.USER_SETTINGS_WRITE], "upcoming_shifts": [RBACPermission.Permissions.USER_SETTINGS_READ], + "filters": [RBACPermission.Permissions.USER_SETTINGS_READ], } rbac_object_permissions = { @@ -846,6 +847,46 @@ class UserView( return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) + @extend_schema( + responses=inline_serializer( + name="UserFilters", + fields={ + "name": serializers.CharField(), + "type": serializers.CharField(), + "href": serializers.CharField(required=False), + "global": serializers.BooleanField(required=False), + "default": serializers.JSONField(required=False), + "description": serializers.CharField(required=False), + "options": inline_serializer( + name="UserFiltersOptions", + fields={ + "value": serializers.CharField(), + "display_name": serializers.IntegerField(), + }, + ), + }, + many=True, + ) + ) + @action(methods=["get"], detail=False) + def filters(self, request): + filter_name = request.query_params.get("search", None) + api_root = "/api/internal/v1/" + + filter_options = [ + { + "name": "team", + "type": "team_select", + "href": api_root + "teams/", + "global": True, + }, + ] + + if filter_name is not None: + filter_options = list(filter(lambda f: filter_name in f["name"], filter_options)) + + return Response(filter_options) + def handle_phone_notificator_failed(exc: BaseFailed) -> Response: if exc.graceful_msg: diff --git a/grafana-plugin/e2e-tests/users/usersActions.test.ts b/grafana-plugin/e2e-tests/users/usersActions.test.ts index 3134f262..f106abd9 100644 --- a/grafana-plugin/e2e-tests/users/usersActions.test.ts +++ b/grafana-plugin/e2e-tests/users/usersActions.test.ts @@ -56,9 +56,13 @@ test.describe('Users screen actions', () => { await page.waitForTimeout(2000); - const searchInput = page.locator(`[data-testid="search-users"]`); - - await searchInput.fill(userName); + await page + .locator('div') + .filter({ hasText: /^Search or filter results\.\.\.$/ }) + .nth(1) + .click(); + await page.keyboard.insertText(userName); + await page.keyboard.press('Enter'); await page.waitForTimeout(2000); const result = page.locator(`[data-testid="users-username"]`); diff --git a/grafana-plugin/src/models/user/user.helpers.tsx b/grafana-plugin/src/models/user/user.helpers.tsx index 3588bb87..501b8e1b 100644 --- a/grafana-plugin/src/models/user/user.helpers.tsx +++ b/grafana-plugin/src/models/user/user.helpers.tsx @@ -35,9 +35,8 @@ export class UserHelper { * NOTE: if is_currently_oncall=all the backend will not paginate the results, it will send back an array of ALL users */ static async search(f: any = { searchTerm: '' }, page = 1) { - const filters = typeof f === 'string' ? { searchTerm: f } : f; // for GSelect compatibility - const { searchTerm: search, ...restFilters } = filters; - return (await onCallApi().GET('/users/', { params: { query: { search, page, ...restFilters } } })).data; + const filters = typeof f === 'string' ? { search: f } : f; // for GSelect compatibility + return (await onCallApi().GET('/users/', { params: { query: { ...filters, page } } })).data; } static getSearchResult(userStore: UserStore) { diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index 34a42f25..df5afbdb 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -3,6 +3,7 @@ import dayjs from 'dayjs'; import { get } from 'lodash-es'; import { action, computed, runInAction, makeAutoObservable } from 'mobx'; +import { RemoteFiltersType } from 'containers/RemoteFilters/RemoteFilters.types'; import { ActionKey } from 'models/loader/action-keys'; import { NotificationPolicyType } from 'models/notification_policy/notification_policy'; import { makeRequest } from 'network/network'; @@ -36,7 +37,11 @@ export class UserStore { this.rootStore = rootStore; } - async fetchItems(f: any = { searchTerm: '' }, page = 1, invalidateFn?: () => boolean): Promise { + async fetchItems( + f: RemoteFiltersType | string = { searchTerm: '', type: undefined, used: undefined }, + page = 1, + invalidateFn?: () => boolean + ): Promise { const response = await UserHelper.search(f, page); if (invalidateFn && invalidateFn()) { diff --git a/grafana-plugin/src/pages/users/Users.styles.ts b/grafana-plugin/src/pages/users/Users.styles.ts index bd439484..b8b48c22 100644 --- a/grafana-plugin/src/pages/users/Users.styles.ts +++ b/grafana-plugin/src/pages/users/Users.styles.ts @@ -3,6 +3,10 @@ import { GrafanaTheme2 } from '@grafana/data'; export const getUsersStyles = (theme: GrafanaTheme2) => { return { + filters: css` + margin-bottom: 20px; + `, + usersTtitle: css` display: flex; align-items: center; diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index ed9c9817..cf18d789 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -18,7 +18,8 @@ import { import { PluginLink } from 'components/PluginLink/PluginLink'; import { Text } from 'components/Text/Text'; import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge'; -import { UsersFilters } from 'components/UsersFilters/UsersFilters'; +import { RemoteFilters } from 'containers/RemoteFilters/RemoteFilters'; +import { RemoteFiltersType } from 'containers/RemoteFilters/RemoteFilters.types'; import { UserSettings } from 'containers/UserSettings/UserSettings'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { UserHelper } from 'models/user/user.helpers'; @@ -44,9 +45,8 @@ const REQUIRED_PERMISSION_TO_VIEW_USERS = UserActions.UserSettingsWrite; interface UsersState extends PageBaseState { isWrongTeam: boolean; userPkToEdit?: ApiSchemas['User']['pk'] | 'new'; - usersFilters?: { - searchTerm: string; - }; + + filters: RemoteFiltersType; } @observer @@ -62,9 +62,7 @@ class Users extends React.Component { this.state = { isWrongTeam: false, userPkToEdit: undefined, - usersFilters: { - searchTerm: '', - }, + filters: { searchTerm: '', type: undefined, used: undefined, mine: undefined }, errorData: initErrorDataState(), }; @@ -80,7 +78,7 @@ class Users extends React.Component { updateUsers = debounce(async (invalidateFn?: () => boolean) => { const { store } = this.props; - const { usersFilters } = this.state; + const { filters } = this.state; const { userStore, filtersStore } = store; const page = filtersStore.currentTablePageNum[PAGE.Users]; @@ -89,7 +87,7 @@ class Users extends React.Component { } LocationHelper.update({ p: page }, 'partial'); - await userStore.fetchItems(usersFilters, page, invalidateFn); + await userStore.fetchItems(filters, page, invalidateFn); this.forceUpdate(); }, DEBOUNCE_MS); @@ -184,38 +182,20 @@ class Users extends React.Component { renderContentIfAuthorized(authorizedToViewUsers: boolean) { const { store: { userStore, filtersStore }, - theme, } = this.props; - const { usersFilters, userPkToEdit } = this.state; + const { userPkToEdit } = this.state; const page = filtersStore.currentTablePageNum[PAGE.Users]; const { count, results, page_size } = UserHelper.getSearchResult(userStore); const columns = this.getTableColumns(); - const handleClear = () => - this.setState({ usersFilters: { searchTerm: '' } }, () => { - this.updateUsers(); - }); - const styles = getUsersStyles(theme); - return ( <> {authorizedToViewUsers ? ( <> -
- - -
- + {this.renderFilters()} { ); } + renderFilters() { + const { query, store, theme } = this.props; + const styles = getUsersStyles(theme); + + return ( +
+ +
+ ); + } + + handleFiltersChange = (filters: RemoteFiltersType, _isOnMount: boolean) => { + const { filtersStore } = this.props.store; + const currentTablePage = filtersStore.currentTablePageNum[PAGE.Users]; + + LocationHelper.update({ p: currentTablePage }, 'partial'); + + this.setState({ filters }, () => { + this.updateUsers(); + }); + }; + renderTitle = (user: ApiSchemas['User']) => { const { store: { userStore }, @@ -288,18 +295,6 @@ class Users extends React.Component { return user.notification_chain_verbal.important; }; - renderContacts = (user: ApiSchemas['User']) => { - const { store } = this.props; - return ( -
-
Slack: {user.slack_user_identity?.name || '-'}
- {store.hasFeature(AppFeature.Telegram) && ( -
Telegram: {user.telegram_configuration?.telegram_nick_name || '-'}
- )} -
- ); - }; - renderButtons = (user: ApiSchemas['User']) => { const { store } = this.props; const { userStore } = store; @@ -442,16 +437,6 @@ class Users extends React.Component { this.updateUsers(); }; - handleUsersFiltersChange = (usersFilters: any, invalidateFn: () => boolean) => { - const { filtersStore } = this.props.store; - - filtersStore.currentTablePageNum[PAGE.Users] = 1; - - this.setState({ usersFilters }, () => { - this.updateUsers(invalidateFn); - }); - }; - handleHideUserSettings = () => { const { history } = this.props; this.setState({ userPkToEdit: undefined }); From 37f661a708ff845e225812f4053a69f89cf10f25 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 8 Jul 2024 10:24:20 +0300 Subject: [PATCH 2/9] Fix column caret display on schedules (#4612) # What this PR does - Fixed column spacing ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall/issues/4480 --- grafana-plugin/src/components/GTable/GTable.tsx | 8 +------- grafana-plugin/src/components/Table/Table.styles.ts | 6 +----- .../ServiceNowConfigDrawer/ServiceNowTokenSection.tsx | 2 +- .../src/pages/schedules/Schedules.styles.ts | 8 ++++++++ grafana-plugin/src/pages/schedules/Schedules.tsx | 11 ++++++++--- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/grafana-plugin/src/components/GTable/GTable.tsx b/grafana-plugin/src/components/GTable/GTable.tsx index b251a4ee..0ba97846 100644 --- a/grafana-plugin/src/components/GTable/GTable.tsx +++ b/grafana-plugin/src/components/GTable/GTable.tsx @@ -135,7 +135,7 @@ export const GTable = (props: }, [rowSelection, columnsProp, data]); return ( -
+
expandable={expandable} rowKey={rowKey} @@ -162,12 +162,6 @@ const getGTableStyles = () => ({ } `, - fixed: css` - table { - table-layout: fixed; - } - `, - pagination: css` margin-top: 20px; `, diff --git a/grafana-plugin/src/components/Table/Table.styles.ts b/grafana-plugin/src/components/Table/Table.styles.ts index e244cf53..6ac89877 100644 --- a/grafana-plugin/src/components/Table/Table.styles.ts +++ b/grafana-plugin/src/components/Table/Table.styles.ts @@ -10,10 +10,6 @@ export const getTableStyles = (theme: GrafanaTheme2) => { width: 100%; } - table :global(.rc-table-row-expand-icon-cell) > span { - pointer-events: none; - } - tr { min-height: 56px; } @@ -43,7 +39,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => { transition: transform 0.2s; &--expanded { - transform: rotate(0deg); + transform: rotate(0deg) translateY(-5px); // to manually compensate for top: 3px } `, diff --git a/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowTokenSection.tsx b/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowTokenSection.tsx index 9f48f489..bdeb1cad 100644 --- a/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowTokenSection.tsx +++ b/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowTokenSection.tsx @@ -87,7 +87,7 @@ export const ServiceNowTokenSection: React.FC = obs function renderGenerateButton() { return ( - ); diff --git a/grafana-plugin/src/pages/schedules/Schedules.styles.ts b/grafana-plugin/src/pages/schedules/Schedules.styles.ts index b2c93906..277b3563 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.styles.ts +++ b/grafana-plugin/src/pages/schedules/Schedules.styles.ts @@ -6,6 +6,14 @@ export const getSchedulesStyles = () => { position: relative; `, + tableRoot: css` + td.rc-table-row-expand-icon-cell { + position: relative; + top: 5px; + left: 3px; + } + `, + table: css` td { padding-top: 5px; diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index de49ee6d..bdf58c8d 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -105,7 +105,7 @@ class _SchedulesPage extends React.Component
-
+
) + width: '40px', + title: '', + render: () => <>, + }, { width: '10%', title: 'Type', From 038d62788f66c5123174bd0efe41fa0ecd97c671 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 8 Jul 2024 12:03:26 +0300 Subject: [PATCH 3/9] Leftover tweaks (#4615) # What this PR does - Moved `Custom` as the last option for silencing period - Added placeholder for "Select or type" on dropdowns where input is allowed (such as custom wait durations) - Cleanup (removed `SilenceCascaderButton` component) --- .../components/Policy/EscalationPolicy.tsx | 4 +- .../components/Policy/NotificationPolicy.tsx | 1 - .../src/components/Policy/Policy.consts.ts | 2 +- .../src/pages/incident/Incident.helpers.tsx | 4 +- .../src/pages/incidents/Incidents.tsx | 4 +- .../incidents/parts/SilenceButtonCascader.tsx | 40 ------------------- .../pages/incidents/parts/SilenceSelect.tsx | 4 +- 7 files changed, 10 insertions(+), 49 deletions(-) delete mode 100644 grafana-plugin/src/pages/incidents/parts/SilenceButtonCascader.tsx diff --git a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx index 95212384..c91700e9 100644 --- a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx @@ -279,7 +279,7 @@ class _EscalationPolicy extends React.Component { Date: Mon, 8 Jul 2024 12:25:14 +0200 Subject: [PATCH 4/9] fix faro reporting errors from local env (#4621) # What this PR does fix faro reporting errors from local env ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- grafana-plugin/src/utils/consts.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index 7e26bae2..a091378e 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -35,23 +35,24 @@ export const ONCALL_PROD = 'https://oncall-prod-us-central-0.grafana.net/oncall' export const ONCALL_OPS = 'https://oncall-ops-us-east-0.grafana.net/oncall'; export const ONCALL_DEV = 'https://oncall-dev-us-central-0.grafana.net/oncall'; -export const getProcessEnvVarSafely = (name: string) => { +export const getIsDevelopmentEnv = () => { try { - return process.env[name]; + return process.env.NODE_ENV === 'development'; } catch (error) { - console.error(error); - return undefined; + return false; } }; -export const getIsDevelopmentEnv = () => getProcessEnvVarSafely['NODE_ENV'] === 'development'; - // Single source of truth on the frontend for OnCall API URL export const getOnCallApiUrl = (meta?: OnCallAppPluginMeta) => { if (meta?.jsonData?.onCallApiUrl) { return meta?.jsonData?.onCallApiUrl; } else if (typeof window === 'undefined') { - return getProcessEnvVarSafely('ONCALL_API_URL'); + try { + return process.env.ONCALL_API_URL; + } catch (error) { + return undefined; + } } return undefined; }; From 3f8087281c21a5ebacf602eee3df17b6f6e4a48d Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Mon, 8 Jul 2024 13:25:29 +0200 Subject: [PATCH 5/9] remove duplicated Table component (#4625) # What this PR does remove duplicated Table component ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall/issues/4623 ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- .../src/components/GTable/GTable.tsx | 13 ++-- .../src/components/Table/Table.styles.ts | 50 ------------- grafana-plugin/src/components/Table/Table.tsx | 74 ------------------- .../src/pages/schedules/Schedules.styles.ts | 1 - .../src/pages/schedules/Schedules.tsx | 7 +- 5 files changed, 10 insertions(+), 135 deletions(-) delete mode 100644 grafana-plugin/src/components/Table/Table.styles.ts delete mode 100644 grafana-plugin/src/components/Table/Table.tsx diff --git a/grafana-plugin/src/components/GTable/GTable.tsx b/grafana-plugin/src/components/GTable/GTable.tsx index 0ba97846..65a21601 100644 --- a/grafana-plugin/src/components/GTable/GTable.tsx +++ b/grafana-plugin/src/components/GTable/GTable.tsx @@ -19,7 +19,7 @@ export interface GTableProps extends TableProps React.ReactNode; - onExpandedRowsChange: (rows: string[]) => void; + onExpandedRowsChange?: (rows: string[]) => void; expandRowByClick: boolean; expandIcon?: (props: { expanded: boolean; record: any }) => React.ReactNode; onExpand?: (expanded: boolean, item: any) => void; @@ -47,7 +47,7 @@ export const GTable = (props: const { expanded, record } = props; return ( { event.stopPropagation(); @@ -61,8 +61,8 @@ export const GTable = (props: newExpandedRowKeys.splice(index, 1); } - expandable.onExpand && expandable.onExpand(newExpanded, record); - expandable.onExpandedRowsChange(newExpandedRowKeys); + expandable.onExpand?.(newExpanded, record); + expandable.onExpandedRowsChange?.(newExpandedRowKeys); }} /> ); @@ -161,12 +161,13 @@ const getGTableStyles = () => ({ width: 100%; } `, - pagination: css` margin-top: 20px; `, - checkbox: css` display: inline-flex; `, + expandIcon: css` + cursor: pointer; + `, }); diff --git a/grafana-plugin/src/components/Table/Table.styles.ts b/grafana-plugin/src/components/Table/Table.styles.ts deleted file mode 100644 index 6ac89877..00000000 --- a/grafana-plugin/src/components/Table/Table.styles.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { css } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data'; - -export const getTableStyles = (theme: GrafanaTheme2) => { - return { - root: css` - width: 100%; - - table { - width: 100%; - } - - tr { - min-height: 56px; - } - - th:first-child { - padding-left: 20px; - } - - td { - min-height: 60px; - padding-top: 10px; - padding-bottom: 10px; - } - `, - - pagination: css` - width: 100%; - margin-top: 20px; - `, - - expandIcon: css` - padding: 10px; - color: ${theme.colors.text.primary}; - pointer-events: none; - transform: rotate(-90deg); - transform-origin: center; - transition: transform 0.2s; - - &--expanded { - transform: rotate(0deg) translateY(-5px); // to manually compensate for top: 3px - } - `, - - rowEven: css` - background: ${theme.colors.background.secondary}; - `, - }; -}; diff --git a/grafana-plugin/src/components/Table/Table.tsx b/grafana-plugin/src/components/Table/Table.tsx deleted file mode 100644 index cf2883e2..00000000 --- a/grafana-plugin/src/components/Table/Table.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { FC, useMemo } from 'react'; - -import { cx } from '@emotion/css'; -import { Pagination, VerticalGroup, useStyles2 } from '@grafana/ui'; -import Table from 'rc-table'; -import { TableProps } from 'rc-table/lib/Table'; -import { bem } from 'styles/utils.styles'; - -import { ExpandIcon } from 'icons/Icons'; - -import { getTableStyles } from './Table.styles'; - -export interface Props extends TableProps { - loading?: boolean; - pagination?: { - page: number; - total: number; - onChange: (page: number) => void; - }; - rowSelection?: { - selectedRowKeys: string[]; - onChange: (selectedRowKeys: string[]) => void; - }; - expandable?: { - expandedRowKeys: string[]; - expandedRowRender: (item: any) => React.ReactNode; - onExpandedRowsChange?: (rows: string[]) => void; - expandRowByClick: boolean; - expandIcon?: (props: { expanded: boolean; record: any }) => React.ReactNode; - onExpand?: (expanded: boolean, item: any) => void; - }; -} - -export const GTable: FC = (props) => { - const { columns, data, className, pagination, loading, rowKey, expandable, ...restProps } = props; - const { page, total: numberOfPages, onChange: onNavigate } = pagination || {}; - - const styles = useStyles2(getTableStyles); - - const expandableFn = useMemo(() => { - return expandable - ? { - ...expandable, - expandIcon: ({ expanded }) => { - return ( -
- -
- ); - }, - expandedRowClassName: (_record, index) => (index % 2 === 0 ? styles.rowEven : ''), - } - : null; - }, [expandable]); - - return ( - - (index % 2 === 0 ? styles.rowEven : '')} - {...restProps} - /> - {pagination && ( -
- -
- )} - - ); -}; diff --git a/grafana-plugin/src/pages/schedules/Schedules.styles.ts b/grafana-plugin/src/pages/schedules/Schedules.styles.ts index 277b3563..66da6756 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.styles.ts +++ b/grafana-plugin/src/pages/schedules/Schedules.styles.ts @@ -9,7 +9,6 @@ export const getSchedulesStyles = () => { tableRoot: css` td.rc-table-row-expand-icon-cell { position: relative; - top: 5px; left: 3px; } `, diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index bdf58c8d..390abaa7 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -9,9 +9,9 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import { getUtilStyles } from 'styles/utils.styles'; import { Avatar } from 'components/Avatar/Avatar'; +import { GTable, GTableProps } from 'components/GTable/GTable'; import { NewScheduleSelector } from 'components/NewScheduleSelector/NewScheduleSelector'; import { PluginLink } from 'components/PluginLink/PluginLink'; -import { GTable } from 'components/Table/Table'; import { Text } from 'components/Text/Text'; import { TextEllipsisTooltip } from 'components/TextEllipsisTooltip/TextEllipsisTooltip'; import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge'; @@ -62,7 +62,6 @@ class _SchedulesPage extends React.Component{item.name}; }; - renderOncallNow = (item: Schedule, _index: number) => { + renderOncallNow = (item: Schedule) => { const { theme } = this.props; const utilsStyles = getUtilStyles(theme); @@ -406,7 +405,7 @@ class _SchedulesPage extends React.Component { + getTableColumns = (): GTableProps['columns'] => { const { grafanaTeamStore } = this.props.store; const styles = getSchedulesStyles(); From 22556f49052ecac5660f4f9261dfd527e6efe655 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 8 Jul 2024 15:18:06 +0300 Subject: [PATCH 6/9] Webhook form - changed Forward all (#4622) # What this PR does - added validation for `Data` field, backend seems to be a bit clumsy on validating that field - Changed `Fordwar All` / `Customise data` to be more straightforward for the user ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall/issues/4412 --- .../OutgoingWebhookForm.tsx | 4 + .../OutgoingWebhookFormFields.tsx | 128 +++++++++++------- 2 files changed, 82 insertions(+), 50 deletions(-) diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx index 8dc52c59..d323aa0f 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx @@ -78,6 +78,10 @@ function prepareForSave(rawData: Partial, selectedPreset: delete data[field]; }); + if (data.forward_all) { + data.data = null; + } + return data; } diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookFormFields.tsx b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookFormFields.tsx index c493acb6..02042de8 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookFormFields.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookFormFields.tsx @@ -1,12 +1,13 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; -import { Button, Field, Input, Select, Switch, useStyles2 } from '@grafana/ui'; +import { Button, Field, Input, RadioButtonList, Select, Switch, useStyles2 } from '@grafana/ui'; import Emoji from 'react-emoji-render'; import { Controller, useFormContext } from 'react-hook-form'; import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor'; import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.config'; +import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; import { GSelect } from 'containers/GSelect/GSelect'; import { Labels } from 'containers/Labels/Labels'; import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; @@ -29,6 +30,21 @@ interface OutgoingWebhookFormFieldsProps { onTemplateEditClick: (params: TemplateParams) => void; } +const FORWARD = 'forward'; +const CUSTOMIZE = 'customise'; +const FORWARD_RADIO_OPTIONS = [ + { + label: 'Forward whole payload data', + value: FORWARD, + boolean: true, + }, + { + label: 'Customise forwarded data', + value: CUSTOMIZE, + boolean: false, + }, +]; + export const OutgoingWebhookFormFields = ({ preset, hasLabelsFeature, @@ -42,7 +58,6 @@ export const OutgoingWebhookFormFields = ({ } = useFormContext(); const forwardAll = watch(WebhookFormFieldName.ForwardAll); - const styles = useStyles2(getStyles); const controls = ( @@ -175,6 +190,7 @@ export const OutgoingWebhookFormFields = ({ /> )} ( @@ -299,55 +315,67 @@ export const OutgoingWebhookFormFields = ({ )} /> - ( - - - - )} - /> - ( - -
-
- -
-
-
- )} - /> + )} + /> + + + ( + ( + +
+
+ +
+
+
+ )} + /> + )} + /> +
); From 6fec1eb43a973cdcd0c1120ad4aed27893999443 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Mon, 8 Jul 2024 11:02:32 -0300 Subject: [PATCH 7/9] Handle unexpected recipient value. Add logging. (#4626) Related to https://github.com/grafana/oncall-private/issues/2683 --- engine/apps/email/inbound.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/engine/apps/email/inbound.py b/engine/apps/email/inbound.py index 5a37daa3..76eec2aa 100644 --- a/engine/apps/email/inbound.py +++ b/engine/apps/email/inbound.py @@ -86,7 +86,13 @@ class InboundEmailWebhookView(AlertChannelDefiningMixin, APIView): # First try envelope_recipient field. # According to AnymailInboundMessage it's provided not by all ESPs. if message.envelope_recipient: - token, domain = message.envelope_recipient.split("@") + try: + token, domain = message.envelope_recipient.split("@") + except ValueError: + logger.error( + f"get_integration_token_from_request: envelope_recipient field has unexpected format: {message.envelope_recipient}" + ) + return None if domain == live_settings.INBOUND_EMAIL_DOMAIN: return token else: From 25031b3463c6cb1dce8a7acc7ee7cf7cb6b31a19 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 8 Jul 2024 16:33:58 +0100 Subject: [PATCH 8/9] Tweak chatops-proxy sync (#4627) # What this PR does Fix for https://raintank-corp.slack.com/archives/C04JCU51NF8/p1719452618841629. * Move chatops-proxy sync tasks to the long queue * Batch tasks slower (12 hours instead of 30 minutes) ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- engine/apps/chatops_proxy/tasks.py | 2 +- engine/settings/celery_task_routes.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/engine/apps/chatops_proxy/tasks.py b/engine/apps/chatops_proxy/tasks.py index 785233e3..0e93500d 100644 --- a/engine/apps/chatops_proxy/tasks.py +++ b/engine/apps/chatops_proxy/tasks.py @@ -152,7 +152,7 @@ def start_sync_org_with_chatops_proxy(): organization_qs = Organization.objects.all() organization_pks = organization_qs.values_list("pk", flat=True) - max_countdown = 60 * 30 # 30 minutes, feel free to adjust + max_countdown = 12 * 60 * 60 # 12 hours, feel free to adjust for idx, organization_pk in enumerate(organization_pks): countdown = idx % max_countdown sync_org_with_chatops_proxy.apply_async(kwargs={"org_id": organization_pk}, countdown=countdown) diff --git a/engine/settings/celery_task_routes.py b/engine/settings/celery_task_routes.py index 01d4a155..9d8e0537 100644 --- a/engine/settings/celery_task_routes.py +++ b/engine/settings/celery_task_routes.py @@ -91,8 +91,6 @@ CELERY_TASK_ROUTES = { "apps.chatops_proxy.tasks.unlink_slack_team_async": {"queue": "default"}, "apps.chatops_proxy.tasks.register_oncall_tenant_async": {"queue": "default"}, "apps.chatops_proxy.tasks.unregister_oncall_tenant_async": {"queue": "default"}, - "apps.chatops_proxy.tasks.start_sync_org_with_chatops_proxy": {"queue": "default"}, - "apps.chatops_proxy.tasks.sync_org_with_chatops_proxy": {"queue": "default"}, # CRITICAL "apps.alerts.tasks.acknowledge_reminder.acknowledge_reminder_task": {"queue": "critical"}, "apps.alerts.tasks.acknowledge_reminder.unacknowledge_timeout_task": {"queue": "critical"}, @@ -141,6 +139,8 @@ CELERY_TASK_ROUTES = { "apps.alerts.tasks.check_escalation_finished.check_escalation_finished_task": {"queue": "long"}, "apps.alerts.tasks.check_escalation_finished.check_alert_group_personal_notifications_task": {"queue": "long"}, "apps.alerts.tasks.check_escalation_finished.check_personal_notifications_task": {"queue": "long"}, + "apps.chatops_proxy.tasks.start_sync_org_with_chatops_proxy": {"queue": "long"}, + "apps.chatops_proxy.tasks.sync_org_with_chatops_proxy": {"queue": "long"}, "apps.grafana_plugin.tasks.sync.cleanup_organization_async": {"queue": "long"}, "apps.grafana_plugin.tasks.sync.cleanup_empty_deleted_integrations": {"queue": "long"}, "apps.grafana_plugin.tasks.sync.start_cleanup_organizations": {"queue": "long"}, From 0163b58399733e7855cd96869d9a967d2497899b Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 8 Jul 2024 11:52:20 -0400 Subject: [PATCH 9/9] notify user task patch + small update to user notification rules public API docs (#4628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What this PR does Patches a small bug noticed (locally) by @Ferril 🙏 + updates our user notification rules public API docs to include `notify_by_msteams` as a valid `type` value (cloud only) ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- .../personal_notification_rules.md | 2 +- engine/apps/alerts/tasks/notify_user.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/sources/oncall-api-reference/personal_notification_rules.md b/docs/sources/oncall-api-reference/personal_notification_rules.md index 422749b2..e683e90b 100644 --- a/docs/sources/oncall-api-reference/personal_notification_rules.md +++ b/docs/sources/oncall-api-reference/personal_notification_rules.md @@ -35,7 +35,7 @@ The above command returns JSON structured in the following way: | ----------- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `user_id` | Yes | User ID | | `position` | Optional | Personal notification rules execute one after another starting from `position=0`. `Position=-1` will put the escalation policy to the end of the list. A new escalation policy created with a position of an existing escalation policy will move the old one (and all following) down on the list. | -| `type` | Yes | One of: `wait`, `notify_by_slack`, `notify_by_sms`, `notify_by_phone_call`, `notify_by_telegram`, `notify_by_email`, `notify_by_mobile_app`, `notify_by_mobile_app_critical`. | +| `type` | Yes | One of: `wait`, `notify_by_slack`, `notify_by_sms`, `notify_by_phone_call`, `notify_by_telegram`, `notify_by_email`, `notify_by_mobile_app`, `notify_by_mobile_app_critical`, or `notify_by_msteams` (**NOTE** `notify_by_msteams` is only available on Grafana Cloud). | | `duration` | Optional | A time in seconds to wait (when `type=wait`). Can be one of 60, 300, 900, 1800, or 3600. | | `important` | Optional | Boolean value indicates if a rule is "important". Default is `false`. | diff --git a/engine/apps/alerts/tasks/notify_user.py b/engine/apps/alerts/tasks/notify_user.py index 2a197338..1f7b7c1c 100644 --- a/engine/apps/alerts/tasks/notify_user.py +++ b/engine/apps/alerts/tasks/notify_user.py @@ -81,10 +81,12 @@ def notify_user_task( # Here we collect a brief overview of notification steps configured for user to send it to thread. collected_steps_ids = [] - for notification_policy in notification_policies: - if notification_policy.step == UserNotificationPolicy.Step.NOTIFY: - if notification_policy.notify_by not in collected_steps_ids: - collected_steps_ids.append(notification_policy.notify_by) + for next_notification_policy in notification_policies: + if next_notification_policy.step == UserNotificationPolicy.Step.NOTIFY: + if next_notification_policy.notify_by not in collected_steps_ids: + collected_steps_ids.append(next_notification_policy.notify_by) + + notification_policy = notification_policies[0] collected_steps = ", ".join( UserNotificationPolicy.NotificationChannel(step_id).label for step_id in collected_steps_ids